Skip to content

Commit

Permalink
refactor(dto): better structure and composition for dto (#51)
Browse files Browse the repository at this point in the history
  • Loading branch information
antoinezanardi committed Mar 23, 2023
1 parent 0afb1ad commit 3bc3b6e
Show file tree
Hide file tree
Showing 66 changed files with 11,283 additions and 6,367 deletions.
1 change: 1 addition & 0 deletions config/jest/jest-global.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ const config: Config = {
"src/**/*.ts",
"!src/main.ts",
"!src/**/*dto.ts",
"!src/**/*.schema.ts",
],
coverageDirectory: "tests/coverage",
coverageThreshold: {
Expand Down
9 changes: 9 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@
"@types/validator": "^13.7.14",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.0",
"isobject": "^4.0.0",
"lodash": "^4.17.21",
"mongoose": "^6.10.4",
"qs": "^6.11.1",
Expand Down
6 changes: 3 additions & 3 deletions src/modules/game/controllers/game.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import { ApiOperation, ApiResponse, ApiTags } from "@nestjs/swagger";
import { API_RESOURCES } from "../../../shared/api/enums/api.enum";
import { ValidateMongoId } from "../../../shared/api/pipes/validate-mongo-id.pipe";
import { getControllerRouteError } from "../../../shared/error/helpers/error.helper";
import { CreateGamePlayerDto } from "../dto/create-game/create-game-player/create-game-player.dto";
import { CreateGameDto } from "../dto/create-game/create-game.dto";
import { GetGameRandomCompositionPlayerResponseDto } from "../dto/get-game-random-composition/get-game-random-composition-player-response/get-game-random-composition-player-response.dto";
import { GetGameRandomCompositionDto } from "../dto/get-game-random-composition/get-game-random-composition.dto";
import { GAME_STATUSES } from "../enums/game.enum";
import { GameRandomCompositionService } from "../providers/services/game-random-composition.service";
Expand All @@ -30,8 +30,8 @@ export class GameController {

@Get("random-composition")
@ApiOperation({ summary: "Get game random composition for given players" })
@ApiResponse({ status: HttpStatus.OK, type: CreateGamePlayerDto, isArray: true })
public getGameRandomComposition(@Query() getGameRandomCompositionDto: GetGameRandomCompositionDto): CreateGamePlayerDto[] {
@ApiResponse({ status: HttpStatus.OK, type: GetGameRandomCompositionPlayerResponseDto, isArray: true })
public getGameRandomComposition(@Query() getGameRandomCompositionDto: GetGameRandomCompositionDto): GetGameRandomCompositionPlayerResponseDto[] {
return this.gameRandomCompositionService.getGameRandomComposition(getGameRandomCompositionDto);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { applyDecorators } from "@nestjs/common";
import { ArrayMaxSize, ArrayMinSize } from "class-validator";
import { gameFieldsSpecs } from "../../../constants/game.constant";

function CompositionBounds(): <TFunction extends () => void, Y>(target: (TFunction | object), propertyKey?: (string | symbol), descriptor?: TypedPropertyDescriptor<Y>) => void {
return applyDecorators(
ArrayMinSize(gameFieldsSpecs.players.minItems),
ArrayMaxSize(gameFieldsSpecs.players.maxItems),
);
}

export { CompositionBounds };
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
import type { ValidationOptions } from "class-validator";
import { registerDecorator } from "class-validator";
import isObject from "isobject";
import { has } from "lodash";
import { roles } from "../../../../role/constants/role.constant";
import type { ROLE_NAMES } from "../../../../role/enums/role.enum";
import { ROLE_SIDES } from "../../../../role/enums/role.enum";
import type { CreateGamePlayerDto } from "../create-game-player/create-game-player.dto";
import type { CreateGameDto } from "../create-game.dto";

function doesCompositionHaveAtLeastOneVillager(players?: CreateGamePlayerDto[]): boolean {
if (!Array.isArray(players)) {
function doesCompositionHaveAtLeastOneVillager(value?: unknown): boolean {
if (!Array.isArray(value) || value.some(player => !isObject(player) || !has(player, ["role", "name"]))) {
return false;
}
const players = value as { role: { name: ROLE_NAMES } }[];
const werewolfRoles = roles.filter(role => role.side === ROLE_SIDES.VILLAGERS);
return players.some(({ role }) => werewolfRoles.find(werewolfRole => role.name === werewolfRole.name));
}
Expand All @@ -18,7 +20,7 @@ function getCompositionHasVillagerDefaultMessage(): string {
}

function CompositionHasVillager(validationOptions?: ValidationOptions) {
return (object: CreateGameDto, propertyName: keyof CreateGameDto): void => {
return (object: object, propertyName: string): void => {
registerDecorator({
name: "CompositionHasVillager",
target: object.constructor,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
import type { ValidationOptions } from "class-validator";
import { registerDecorator } from "class-validator";
import isObject from "isobject";
import { has } from "lodash";
import { roles } from "../../../../role/constants/role.constant";
import type { ROLE_NAMES } from "../../../../role/enums/role.enum";
import { ROLE_SIDES } from "../../../../role/enums/role.enum";
import type { CreateGamePlayerDto } from "../create-game-player/create-game-player.dto";
import type { CreateGameDto } from "../create-game.dto";

function doesCompositionHaveAtLeastOneWerewolf(players?: CreateGamePlayerDto[]): boolean {
if (!Array.isArray(players)) {
function doesCompositionHaveAtLeastOneWerewolf(value?: unknown): boolean {
if (!Array.isArray(value) || value.some(player => !isObject(player) || !has(player, ["role", "name"]))) {
return false;
}
const players = value as { role: { name: ROLE_NAMES } }[];
const werewolfRoles = roles.filter(role => role.side === ROLE_SIDES.WEREWOLVES);
return players.some(({ role }) => werewolfRoles.find(werewolfRole => role.name === werewolfRole.name));
}
Expand All @@ -18,7 +20,7 @@ function getCompositionHasWerewolfDefaultMessage(): string {
}

function CompositionHasWerewolf(validationOptions?: ValidationOptions) {
return (object: CreateGameDto, propertyName: keyof CreateGameDto): void => {
return (object: object, propertyName: string): void => {
registerDecorator({
name: "CompositionHasWerewolf",
target: object.constructor,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import type { ValidationOptions } from "class-validator";
import { registerDecorator } from "class-validator";
import type { CreateGamePlayerDto } from "../create-game-player/create-game-player.dto";
import type { CreateGameDto } from "../create-game.dto";
import isObject from "isobject";

function doesCompositionHaveConsistentPositions(players?: CreateGamePlayerDto[]): boolean {
if (!Array.isArray(players)) {
function doesCompositionHaveConsistentPositions(value?: unknown): boolean {
if (!Array.isArray(value) || value.some(player => !isObject(player))) {
return false;
}
const players = value as { position?: number }[];
const uniquePositions = players.reduce<number[]>((acc, { position }) => {
if (position !== undefined && !acc.includes(position) && position < players.length) {
acc.push(position);
Expand All @@ -21,7 +21,7 @@ function getCompositionPositionsConsistencyDefaultMessage(): string {
}

function CompositionPositionsConsistency(validationOptions?: ValidationOptions) {
return (object: CreateGameDto, propertyName: keyof CreateGameDto): void => {
return (object: object, propertyName: string): void => {
registerDecorator({
name: "CompositionPositionsConsistency",
target: object.constructor,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import type { ValidationOptions } from "class-validator";
import { registerDecorator } from "class-validator";
import isObject from "isobject";
import { has } from "lodash";
import { roles } from "../../../../role/constants/role.constant";
import type { CreateGamePlayerDto } from "../create-game-player/create-game-player.dto";
import type { CreateGameDto } from "../create-game.dto";
import type { ROLE_NAMES } from "../../../../role/enums/role.enum";

function areCompositionRolesMaxInGameRespected(players?: CreateGamePlayerDto[]): boolean {
if (!Array.isArray(players)) {
function areCompositionRolesMaxInGameRespected(value?: unknown): boolean {
if (!Array.isArray(value) || value.some(player => !isObject(player) || !has(player, ["role", "name"]))) {
return false;
}
const players = value as { role: { name: ROLE_NAMES } }[];
return roles.every(role => {
const roleCount = players.filter(player => player.role.name === role.name).length;
return roleCount <= role.maxInGame;
Expand All @@ -19,7 +21,7 @@ function getCompositionRolesMaxInGameDefaultMessage(): string {
}

function CompositionRolesMaxInGame(validationOptions?: ValidationOptions) {
return (object: CreateGameDto, propertyName: keyof CreateGameDto): void => {
return (object: object, propertyName: string): void => {
registerDecorator({
name: "CompositionRolesMaxInGame",
target: object.constructor,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
import type { ValidationOptions } from "class-validator";
import { registerDecorator } from "class-validator";
import isObject from "isobject";
import { has } from "lodash";
import { roles } from "../../../../role/constants/role.constant";
import type { ROLE_NAMES } from "../../../../role/enums/role.enum";
import type { Role } from "../../../../role/types/role.type";
import type { CreateGamePlayerDto } from "../create-game-player/create-game-player.dto";
import type { CreateGameDto } from "../create-game.dto";

function areCompositionRolesMinInGameRespected(players?: CreateGamePlayerDto[]): boolean {
if (!Array.isArray(players)) {
function areCompositionRolesMinInGameRespected(value?: unknown): boolean {
if (!Array.isArray(value) || value.some(player => !isObject(player) || !has(player, ["role", "name"]))) {
return false;
}
const players = value as { role: { name: ROLE_NAMES } }[];
return roles
.filter((role): role is Role & { minInGame: number } => role.minInGame !== undefined)
.every(role => {
Expand All @@ -22,7 +24,7 @@ function getCompositionRolesMinInGameDefaultMessage(): string {
}

function CompositionRolesMinInGame(validationOptions?: ValidationOptions) {
return (object: CreateGameDto, propertyName: keyof CreateGameDto): void => {
return (object: object, propertyName: string): void => {
registerDecorator({
name: "CompositionRolesMinInGame",
target: object.constructor,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { applyDecorators } from "@nestjs/common";
import { ArrayUnique } from "class-validator";
import isObject from "isobject";
import { has } from "lodash";

function getPlayerName(value?: unknown): unknown {
if (!isObject(value) || !has(value, "name")) {
return value;
}
return (value as { name: unknown }).name;
}

function CompositionUniqueNames(): <TFunction extends () => void, Y>(
target: (TFunction | object),
propertyKey?: (string | symbol),
descriptor?: TypedPropertyDescriptor<Y>
) => void {
return applyDecorators(ArrayUnique(getPlayerName, { message: "players.name must be unique" }));
}

export { getPlayerName, CompositionUniqueNames };
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { ApiProperty } from "@nestjs/swagger";
import { IsBoolean, IsEnum } from "class-validator";
import { ROLE_NAMES } from "../../../../../role/enums/role.enum";
import { playerApiProperties } from "../../../../schemas/player/constants/player.constant";
import { playerRoleApiProperties } from "../../../../schemas/player/schemas/player-role/constants/player-role.constant";

class GamePlayerRoleBaseDto {
@ApiProperty(playerApiProperties.role)
@IsEnum(ROLE_NAMES)
public name: ROLE_NAMES;

@ApiProperty(playerRoleApiProperties.original)
@IsEnum(ROLE_NAMES)
public original: ROLE_NAMES;

@ApiProperty(playerRoleApiProperties.current)
@IsEnum(ROLE_NAMES)
public current: ROLE_NAMES;

@ApiProperty(playerRoleApiProperties.isRevealed)
@IsBoolean()
public isRevealed: boolean;
}

export { GamePlayerRoleBaseDto };
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { ApiProperty } from "@nestjs/swagger";
import { IsEnum } from "class-validator";
import { ROLE_SIDES } from "../../../../../role/enums/role.enum";
import { playerSideApiProperties } from "../../../../schemas/player/schemas/player-side/constants/player-side.constant";

class GamePlayerSideBaseDto {
@ApiProperty(playerSideApiProperties.original)
@IsEnum(ROLE_SIDES)
public original: ROLE_SIDES;

@ApiProperty(playerSideApiProperties.current)
@IsEnum(ROLE_SIDES)
public current: ROLE_SIDES;
}

export { GamePlayerSideBaseDto };
35 changes: 35 additions & 0 deletions src/modules/game/dto/base/game-player/game-player.base.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { ApiProperty } from "@nestjs/swagger";
import { Transform, Type } from "class-transformer";
import { IsInt, IsString, MaxLength, Min, MinLength, ValidateNested } from "class-validator";
import { playerApiProperties, playersFieldsSpecs } from "../../../schemas/player/constants/player.constant";
import { GamePlayerRoleBaseDto } from "./game-player-role/game-player-role.base.dto";
import { GamePlayerSideBaseDto } from "./game-player-side/game-player-side.base.dto";
import { playerRoleTransformer } from "./transformers/player-role.transformer";
import { playerSideTransformer } from "./transformers/player-side.transformer";

class GamePlayerBaseDto {
@ApiProperty(playerApiProperties.name)
@IsString()
@MinLength(playersFieldsSpecs.name.minLength)
@MaxLength(playersFieldsSpecs.name.maxLength)
public name: string;

@ApiProperty(playerApiProperties.role)
@Transform(playerRoleTransformer)
@Type(() => GamePlayerRoleBaseDto)
@ValidateNested()
public role: GamePlayerRoleBaseDto;

@ApiProperty(playerApiProperties.role)
@Transform(playerSideTransformer)
@Type(() => GamePlayerRoleBaseDto)
@ValidateNested()
public side: GamePlayerSideBaseDto;

@ApiProperty(playerApiProperties.position)
@IsInt()
@Min(playersFieldsSpecs.position.minimum)
public position: number;
}

export { GamePlayerBaseDto };
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import type { TransformFnParams } from "class-transformer/types/interfaces";
import { has } from "lodash";
import { roles } from "../../../../../role/constants/role.constant";
import { ROLE_NAMES } from "../../../../../role/enums/role.enum";

function playerRoleTransformer(params: TransformFnParams): unknown {
if (!has(params.value as object, "name")) {
return params.value;
}
const value = params.value as {
name: string;
current: ROLE_NAMES;
original: ROLE_NAMES;
isRevealed: boolean;
};
const role = roles.find(({ name }) => name === value.name);
if (role === undefined) {
return value;
}
value.current = role.name;
value.original = role.name;
value.isRevealed = role.name === ROLE_NAMES.VILLAGER_VILLAGER;
return value;
}

export { playerRoleTransformer };
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import type { TransformFnParams } from "class-transformer/types/interfaces";
import isObject from "isobject";
import { has } from "lodash";
import { roles } from "../../../../../role/constants/role.constant";
import type { ROLE_SIDES, ROLE_NAMES } from "../../../../../role/enums/role.enum";

function playerSideTransformer(params: TransformFnParams): unknown {
if (!isObject(params.value) || !isObject(params.obj) || !has(params.obj as object, ["role", "name"])) {
return params.value;
}
const obj = params.obj as { role: { name: ROLE_NAMES } };
const value = params.value as {
current: ROLE_SIDES;
original: ROLE_SIDES;
};
const role = roles.find(({ name }) => name === obj.role.name);
if (role === undefined) {
return value;
}
value.current = role.side;
value.original = role.side;
return params.value;
}

export { playerSideTransformer };

0 comments on commit 3bc3b6e

Please sign in to comment.