Skip to content

Commit

Permalink
feat: add currency field + base api changes (#709)
Browse files Browse the repository at this point in the history
* feat: add currency field + base api changes

* fix: remove a debug msg
  • Loading branch information
niekcandaele authored Nov 23, 2023
1 parent a339c70 commit c6518f9
Show file tree
Hide file tree
Showing 10 changed files with 723 additions and 21 deletions.
8 changes: 1 addition & 7 deletions packages/app-api/src/controllers/GameServerController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ import { Type } from 'class-transformer';
import { IdUuidDTO, IdUuidDTOAPI, ParamId } from '../lib/validators.js';
import { PERMISSIONS } from '@takaro/auth';
import { Response } from 'express';
import { PlayerOnGameserverOutputDTO } from '../service/PlayerOnGameserverService.js';
import { PlayerOnGameserverOutputDTOAPI } from './PlayerOnGameserverController.js';

class GameServerTypesOutputDTOAPI extends APIOutput<GameServerOutputDTO[]> {
@Type(() => GameServerOutputDTO)
Expand Down Expand Up @@ -190,12 +190,6 @@ class BanOutputDTO extends APIOutput<BanDTO[]> {
declare data: BanDTO[];
}

class PlayerOnGameserverOutputDTOAPI extends APIOutput<PlayerOnGameserverOutputDTO[]> {
@Type(() => PlayerOnGameserverOutputDTO)
@ValidateNested({ each: true })
declare data: PlayerOnGameserverOutputDTO[];
}

@OpenAPI({
security: [{ domainAuth: [] }],
})
Expand Down
103 changes: 103 additions & 0 deletions packages/app-api/src/controllers/PlayerOnGameserverController.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import { IsNumber, IsOptional, IsString, IsUUID, ValidateNested } from 'class-validator';
import { ITakaroQuery } from '@takaro/db';
import { APIOutput, apiResponse } from '@takaro/http';
import { AuthenticatedRequest, AuthService } from '../service/AuthService.js';
import { Body, Get, Post, JsonController, UseBefore, Req, Params, Res } from 'routing-controllers';
import { OpenAPI, ResponseSchema } from 'routing-controllers-openapi';
import { Type } from 'class-transformer';
import { ParamId } from '../lib/validators.js';
import { PERMISSIONS } from '@takaro/auth';
import { Response } from 'express';
import { PlayerOnGameServerService, PlayerOnGameserverOutputDTO } from '../service/PlayerOnGameserverService.js';

export class PlayerOnGameserverOutputDTOAPI extends APIOutput<PlayerOnGameserverOutputDTO> {
@Type(() => PlayerOnGameserverOutputDTO)
@ValidateNested()
declare data: PlayerOnGameserverOutputDTO;
}

export class PlayerOnGameserverOutputArrayDTOAPI extends APIOutput<PlayerOnGameserverOutputDTO[]> {
@Type(() => PlayerOnGameserverOutputDTO)
@ValidateNested({ each: true })
declare data: PlayerOnGameserverOutputDTO[];
}

class PlayerOnGameServerSearchInputAllowedFilters {
@IsOptional()
@IsUUID(4, { each: true })
id!: string[];

@IsOptional()
@IsString({ each: true })
gameId!: string;

@IsUUID(4, { each: true })
@IsOptional()
gameServerId!: string;

@IsUUID(4, { each: true })
@IsOptional()
playerId!: string;
}

class PlayerOnGameServerSearchInputDTO extends ITakaroQuery<PlayerOnGameServerSearchInputAllowedFilters> {
@ValidateNested()
@Type(() => PlayerOnGameServerSearchInputAllowedFilters)
declare filters: PlayerOnGameServerSearchInputAllowedFilters;

@ValidateNested()
@Type(() => PlayerOnGameServerSearchInputAllowedFilters)
declare search: PlayerOnGameServerSearchInputAllowedFilters;
}

class PlayerOnGameServerSetCurrencyInputDTO {
@IsNumber()
currency!: number;
}

@OpenAPI({
security: [{ domainAuth: [] }],
})
@JsonController()
export class PlayerOnGameServerController {
@UseBefore(AuthService.getAuthMiddleware([PERMISSIONS.READ_PLAYERS]))
@ResponseSchema(PlayerOnGameserverOutputArrayDTOAPI)
@Post('/gameserver/player/search')
async search(
@Req() req: AuthenticatedRequest,
@Res() res: Response,
@Body() query: PlayerOnGameServerSearchInputDTO
) {
const service = new PlayerOnGameServerService(req.domainId);
const result = await service.find({
...query,
page: res.locals.page,
limit: res.locals.limit,
});
return apiResponse(result.results, {
meta: { total: result.total },
req,
res,
});
}

@UseBefore(AuthService.getAuthMiddleware([PERMISSIONS.READ_PLAYERS]))
@ResponseSchema(PlayerOnGameserverOutputDTOAPI)
@Get('/gameserver/player/:id')
async getOne(@Req() req: AuthenticatedRequest, @Params() params: ParamId) {
const service = new PlayerOnGameServerService(req.domainId);
return apiResponse(await service.findOne(params.id));
}

@UseBefore(AuthService.getAuthMiddleware([PERMISSIONS.MANAGE_PLAYERS]))
@ResponseSchema(PlayerOnGameserverOutputDTOAPI)
@Post('/gameserver/player/:id/currency')
async setCurrency(
@Req() req: AuthenticatedRequest,
@Params() params: ParamId,
@Body() body: PlayerOnGameServerSetCurrencyInputDTO
) {
const service = new PlayerOnGameServerService(req.domainId);
return apiResponse(await service.setCurrency(params.id, body.currency));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { IntegrationTest, SetupGameServerPlayers, expect } from '@takaro/test';

const group = 'PlayerOnGameserverController';

const tests = [
new IntegrationTest<SetupGameServerPlayers.ISetupData>({
group,
snapshot: false,
name: 'Get list of players',
setup: SetupGameServerPlayers.setup,
test: async function () {
const res = await this.client.playerOnGameserver.playerOnGameServerControllerSearch({
sortBy: 'gameId',
filters: {
gameServerId: [this.setupData.gameServer1.id],
},
});
expect(res.data.data.length).to.be.eq(this.setupData.players.length);
},
}),
new IntegrationTest<SetupGameServerPlayers.ISetupData>({
group,
snapshot: false,
name: 'Get one player',
setup: SetupGameServerPlayers.setup,
test: async function () {
const res = await this.client.playerOnGameserver.playerOnGameServerControllerSearch();

const player = res.data.data[0];

const playerRes = await this.client.playerOnGameserver.playerOnGameServerControllerGetOne(player.id);
expect(playerRes.data.data.id).to.be.eq(player.id);
},
}),
new IntegrationTest<SetupGameServerPlayers.ISetupData>({
group,
snapshot: false,
name: 'Can set currency',
setup: SetupGameServerPlayers.setup,
test: async function () {
const res = await this.client.playerOnGameserver.playerOnGameServerControllerSearch();

const player = res.data.data[0];

await this.client.playerOnGameserver.playerOnGameServerControllerSetCurrency(player.id, {
currency: 100,
});
const playerRes = await this.client.playerOnGameserver.playerOnGameServerControllerGetOne(player.id);

expect(playerRes.data.data.currency).to.be.eq(100);
},
}),
new IntegrationTest<SetupGameServerPlayers.ISetupData>({
group,
snapshot: true,
name: 'Rejects negative currency',
setup: SetupGameServerPlayers.setup,
expectedStatus: 400,
test: async function () {
const res = await this.client.playerOnGameserver.playerOnGameServerControllerSearch();

const player = res.data.data[0];

const rejectedRes = await this.client.playerOnGameserver.playerOnGameServerControllerSetCurrency(player.id, {
currency: -100,
});

expect(rejectedRes.data.meta.error.message).to.be.eq('Currency must be positive');

return rejectedRes;
},
}),
];

describe(group, function () {
tests.forEach((test) => {
test.run();
});
});
22 changes: 12 additions & 10 deletions packages/app-api/src/db/playerOnGameserver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ export class PlayerOnGameServerModel extends TakaroModel {
positionY: number;
positionZ: number;

currency: number;

static get relationMappings() {
return {
gameServer: {
Expand Down Expand Up @@ -127,16 +129,16 @@ export class PlayerOnGameServerRepo extends ITakaroRepo<
if (!existing) throw new errors.NotFoundError();

const { query } = await this.getModel();
const res = await query
.updateAndFetchById(id, {
ping: data.ping,
ip: data.ip,
positionX: data.positionX,
positionY: data.positionY,
positionZ: data.positionZ,
})
.returning('*');
return new PlayerOnGameserverOutputDTO().construct(res);
const res = await query.updateAndFetchById(id, {
ping: data.ping,
ip: data.ip,
positionX: data.positionX,
positionY: data.positionY,
positionZ: data.positionZ,
currency: data.currency,
});

return this.findOne(res.id);
}

async findGameAssociations(gameId: string, gameServerId: string) {
Expand Down
2 changes: 2 additions & 0 deletions packages/app-api/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import { EventController } from './controllers/EventController.js';
import { HookWorker } from './workers/hookWorker.js';
import { CronJobWorker } from './workers/cronjobWorker.js';
import { CommandWorker } from './workers/commandWorker.js';
import { PlayerOnGameServerController } from './controllers/PlayerOnGameserverController.js';

export const server = new HTTP(
{
Expand All @@ -49,6 +50,7 @@ export const server = new HTTP(
ExternalAuthController,
DiscordController,
EventController,
PlayerOnGameServerController,
],
},
{
Expand Down
26 changes: 24 additions & 2 deletions packages/app-api/src/service/PlayerOnGameserverService.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { TakaroService } from './Base.js';

import { IsIP, IsNumber, IsOptional, IsString, ValidateNested } from 'class-validator';
import { TakaroDTO, TakaroModelDTO, traceableClass } from '@takaro/util';
import { IsIP, IsNumber, IsOptional, IsString, Min, ValidateNested } from 'class-validator';
import { TakaroDTO, TakaroModelDTO, errors, traceableClass } from '@takaro/util';
import { ITakaroQuery } from '@takaro/db';
import { PaginatedOutput } from '../db/base.js';
import { PlayerOnGameServerModel, PlayerOnGameServerRepo } from '../db/playerOnGameserver.js';
Expand Down Expand Up @@ -38,6 +38,9 @@ export class PlayerOnGameserverOutputDTO extends TakaroModelDTO<PlayerOnGameserv
@IsNumber()
@IsOptional()
ping: number;

@IsNumber()
currency: number;
}

export class PlayerOnGameserverOutputWithRolesDTO extends PlayerOnGameserverOutputDTO {
Expand Down Expand Up @@ -77,6 +80,11 @@ export class PlayerOnGameServerUpdateDTO extends TakaroDTO<PlayerOnGameServerUpd
@IsNumber()
@IsOptional()
ping: number;

@IsNumber()
@IsOptional()
@Min(0)
currency: number;
}

@traceableClass('service:playerOnGameserver')
Expand Down Expand Up @@ -144,4 +152,18 @@ export class PlayerOnGameServerService extends TakaroService<
const resolved = await this.resolveRef(ref, gameserverId);
return this.update(resolved.id, data);
}

async setCurrency(id: string, currency: number) {
try {
const res = await this.repo.update(id, await new PlayerOnGameServerUpdateDTO().construct({ currency }));
return res;
} catch (error) {
if (error instanceof Error) {
if (error.name === 'CheckViolationError' && 'constraint' in error && error.constraint === 'currency_positive') {
throw new errors.BadRequestError('Currency must be positive');
}
}
throw error;
}
}
}
Loading

0 comments on commit c6518f9

Please sign in to comment.