Skip to content

Commit

Permalink
#15 Feature/15 create gungi game (#25)
Browse files Browse the repository at this point in the history
* Implement CreateGungiUsecase and Add method into interface Repository which causing interface change

* Fix format

* added DeepPartial type and removed InitRequest from repo

* Create gungi domain directly instead of thorough by repository

* Add e2e tests for check api post create gungi body

---------

Co-authored-by: Allen Hsiao <weiping@hsiao.ca>
  • Loading branch information
zhihdd and allenh authored Nov 14, 2023
1 parent 6fdd6f2 commit 734db1e
Show file tree
Hide file tree
Showing 15 changed files with 7,049 additions and 8,313 deletions.
14,931 changes: 6,718 additions & 8,213 deletions server/package-lock.json

Large diffs are not rendered by default.

151 changes: 76 additions & 75 deletions server/package.json
Original file line number Diff line number Diff line change
@@ -1,75 +1,76 @@
{
"name": "server",
"version": "0.0.1",
"description": "",
"author": "",
"private": true,
"license": "UNLICENSED",
"scripts": {
"build": "nest build",
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
"start": "nest start",
"start:dev": "nest start --watch",
"start:debug": "nest start --debug --watch",
"start:prod": "node dist/main",
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
"lint-check": "eslint \"{src,apps,libs,test}/**/*.ts\"",
"test:watch": "jest --watch",
"test:cov": "jest --coverage",
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
"test:e2e": "jest --config ./test/jest-e2e.json --runInBand",
"test:unit": "jest --config ./test/unitTest/jest.config.js",
"test:unit:watch": "jest --config ./test/unitTest/jest.config.js --watch",
"test": "npm run test:unit && npm run test:e2e"
},
"dependencies": {
"@nestjs/common": "^9.0.0",
"@nestjs/config": "^2.3.1",
"@nestjs/core": "^9.0.0",
"@nestjs/platform-express": "^9.0.0",
"dotenv": "^16.0.3",
"mongodb": "^5.3.0",
"reflect-metadata": "^0.1.13",
"rxjs": "^7.2.0"
},
"devDependencies": {
"@nestjs/cli": "^9.0.0",
"@nestjs/schematics": "^9.0.0",
"@nestjs/testing": "^9.0.0",
"@types/express": "^4.17.13",
"@types/jest": "29.2.4",
"@types/node": "18.11.18",
"@types/supertest": "^2.0.11",
"@typescript-eslint/eslint-plugin": "^5.0.0",
"@typescript-eslint/parser": "^5.0.0",
"eslint": "^8.22.0",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-prettier": "^4.0.0",
"jest": "29.3.1",
"prettier": "^2.3.2",
"source-map-support": "^0.5.20",
"supertest": "^6.1.3",
"ts-jest": "29.0.3",
"ts-loader": "^9.2.3",
"ts-node": "^10.0.0",
"tsconfig-paths": "4.1.1",
"typescript": "^4.7.4"
},
"jest": {
"moduleFileExtensions": [
"js",
"json",
"ts"
],
"rootDir": "src",
"testRegex": ".*\\.spec\\.ts$",
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
},
"collectCoverageFrom": [
"**/*.(t|j)s"
],
"coverageDirectory": "../coverage",
"testEnvironment": "node"
}
}
{
"name": "server",
"version": "0.0.1",
"description": "",
"author": "",
"private": true,
"license": "UNLICENSED",
"scripts": {
"build": "nest build",
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
"start": "nest start",
"start:dev": "nest start --watch",
"start:debug": "nest start --debug --watch",
"start:prod": "node dist/main",
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
"lint-check": "eslint \"{src,apps,libs,test}/**/*.ts\"",
"test:watch": "jest --watch",
"test:cov": "jest --coverage",
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
"test:e2e": "jest --config ./test/jest-e2e.json --runInBand",
"test:unit": "jest --config ./test/unitTest/jest.config.js",
"test:unit:watch": "jest --config ./test/unitTest/jest.config.js --watch",
"test": "npm run test:unit && npm run test:e2e"
},
"dependencies": {
"@nestjs/common": "^9.0.0",
"@nestjs/config": "^2.3.1",
"@nestjs/core": "^9.0.0",
"@nestjs/platform-express": "^9.0.0",
"dotenv": "^16.0.3",
"mongodb": "^5.3.0",
"reflect-metadata": "^0.1.13",
"rxjs": "^7.2.0",
"zod": "^3.22.4"
},
"devDependencies": {
"@nestjs/cli": "^9.0.0",
"@nestjs/schematics": "^9.0.0",
"@nestjs/testing": "^9.0.0",
"@types/express": "^4.17.13",
"@types/jest": "29.2.4",
"@types/node": "18.11.18",
"@types/supertest": "^2.0.11",
"@typescript-eslint/eslint-plugin": "^5.0.0",
"@typescript-eslint/parser": "^5.0.0",
"eslint": "^8.22.0",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-prettier": "^4.0.0",
"jest": "29.3.1",
"prettier": "^2.3.2",
"source-map-support": "^0.5.20",
"supertest": "^6.1.3",
"ts-jest": "29.0.3",
"ts-loader": "^9.2.3",
"ts-node": "^10.0.0",
"tsconfig-paths": "4.1.1",
"typescript": "^4.7.4"
},
"jest": {
"moduleFileExtensions": [
"js",
"json",
"ts"
],
"rootDir": "src",
"testRegex": ".*\\.spec\\.ts$",
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
},
"collectCoverageFrom": [
"**/*.(t|j)s"
],
"coverageDirectory": "../coverage",
"testEnvironment": "node"
}
}
8 changes: 8 additions & 0 deletions server/src/domain/events/CreateEvent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { Event } from './Event';

export default interface CreateEvent extends Event {
name: 'CreateEvent';
data: {
gungiId: string;
};
}
17 changes: 17 additions & 0 deletions server/src/frameworks/filter/ZodFilter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { ArgumentsHost, Catch, ExceptionFilter } from '@nestjs/common';
import { ZodError } from 'zod';

@Catch(ZodError)
export class ZodFilter<T extends ZodError> implements ExceptionFilter {
catch(exception: T, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse();
const status = 400;

response.status(status).json({
statusCode: 400,
message: exception.message,
errors: exception.errors,
});
}
}
11 changes: 11 additions & 0 deletions server/src/frameworks/pipe/ZodPipe.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { ArgumentMetadata, Injectable, PipeTransform } from '@nestjs/common';

@Injectable()
export class ZodPipe implements PipeTransform {
constructor(private readonly schema: any) {}

transform(value: any, metadata: ArgumentMetadata) {
this.schema.parse(value);
return value;
}
}
22 changes: 22 additions & 0 deletions server/src/gateway/controllers/Gungi.controller.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import { Body, Controller, HttpStatus, Param, Post, Res } from '@nestjs/common';
import CreateUsecase, {
CreateGungiRequest,
} from '../../usecases/service-class/CreateUsecase';
import FurigomaUsecase, {
FurigomaRequest,
} from '../../usecases/service-class/FurigomaUsecase';
Expand All @@ -19,16 +22,35 @@ import ConfigurationUsecase, {
ConfigurationRequest,
} from '../../usecases/service-class/ConfigurationUsecase';
import ConfigurationPresenter from '../presenter/ConfigurationPresenter';
import CreatePresenter from '../presenter/CreatePresenter';
import { ZodPipe } from '../../frameworks/pipe/ZodPipe';
import CreateGungiValidator from '../validation/create-body-validation';

@Controller()
export default class GungiController {
constructor(
private _createUsecase: CreateUsecase,
private _furigomaUsecase: FurigomaUsecase,
private _surrenderUsecase: SurrenderUsecase,
private _arataUsecase: ArataUsecase,
private _configurationUsecase: ConfigurationUsecase,
) {}

@Post('/gungi/create')
async create(
@Body(new ZodPipe(CreateGungiValidator))
body: { players: { id: string; nickname: string }[] },
@Res() res,
) {
const input: CreateGungiRequest = {
...body,
};
const presenter = new CreatePresenter();

const response = await this._createUsecase.present(input, presenter);
return res.status(HttpStatus.OK).send(response);
}

@Post('/gungi/:gungiId/furigoma')
async furigoma(
@Param('gungiId') gungiId: string,
Expand Down
17 changes: 17 additions & 0 deletions server/src/gateway/presenter/CreatePresenter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import Presenter from '../../usecases/Presenter';
import { Event, findByEventName } from '../../domain/events/Event';

interface CreateGungiView {
url: string;
}

export default class CreatePresenter implements Presenter<CreateGungiView> {
present(events: Event[]): CreateGungiView {
const event = findByEventName(events, 'CreateEvent');
const data = event.data;
const gungiId = data.gungiId;
return {
url: `/gungi/${gungiId}`,
};
}
}
3 changes: 3 additions & 0 deletions server/src/gateway/requestBody/createGungiBody.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default class CreateGungiBody {
players: { id: string; nickname: string }[];
}
14 changes: 14 additions & 0 deletions server/src/gateway/validation/create-body-validation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { z } from 'zod';

const playerSchema = z.object({
id: z.string(),
nickname: z.string(),
});

const CreateGungiValidator = z.object({
players: z.array(playerSchema).refine((players) => players.length === 2, {
message: 'The "players" array must contain exactly 2 players.',
}),
});

export default CreateGungiValidator;
3 changes: 3 additions & 0 deletions server/src/main.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { ZodFilter } from './frameworks/filter/ZodFilter';

const PORT = 8000;

async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalFilters(new ZodFilter());
await app.listen(PORT, () => {
console.log(`Server started on port ${PORT}`);
});
}

bootstrap();
3 changes: 3 additions & 0 deletions server/src/usecases/GungiUsecase.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@ import FurigomaUsecase from './service-class/FurigomaUsecase';
import ImplEventBus from '../gateway/eventBus/ImplEventBus';
import ArataUsecase from './service-class/ArataUsecase';
import ConfigurationUsecase from './service-class/ConfigurationUsecase';
import CreateUsecase from './service-class/CreateUsecase';

@Module({
imports: [GungiRepositoryModule],
providers: [
CreateUsecase,
SurrenderUsecase,
FurigomaUsecase,
ArataUsecase,
Expand All @@ -19,6 +21,7 @@ import ConfigurationUsecase from './service-class/ConfigurationUsecase';
},
],
exports: [
CreateUsecase,
SurrenderUsecase,
FurigomaUsecase,
ConfigurationUsecase,
Expand Down
57 changes: 57 additions & 0 deletions server/src/usecases/service-class/CreateUsecase.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import Usecase from '../Usecase';
import Presenter from '../Presenter';
import CreateEvent from '../../domain/events/CreateEvent';
import { Inject, Injectable } from '@nestjs/common';
import IRepository from '../Repository';
import Gungi from '../../domain/Gungi';
import EventBus from '../EventBus';
import { randomUUID } from 'crypto';
import LEVEL from '../../domain/constant/LEVEL';
import Player from '../../domain/Player';
import GungiHan from '../../domain/GungiHan';
import SIDE from '../../domain/constant/SIDE';
import DeadArea from '../../domain/DeadArea';
import GomaOki from '../../domain/GomaOki';

export type CreateGungiRequest = {
players: { id: string; nickname: string }[];
};

@Injectable()
export default class CreateUsecase implements Usecase<CreateGungiRequest> {
constructor(
@Inject('GungiRepository')
private _gungiRepository: IRepository<Gungi>,
@Inject('EventBus')
private _eventBus: EventBus,
) {}

async present<View>(
request: CreateGungiRequest,
presenter: Presenter<View>,
): Promise<View> {
const level = LEVEL.BEGINNER;
const gungi = new Gungi(
randomUUID(),
level,
request.players.map<Player>((player, index) => {
const side = index === 0 ? SIDE.WHITE : SIDE.BLACK;
const deadArea = new DeadArea(side, []);
const gomaOki = new GomaOki(level, side, []);
return new Player(player.id, player.nickname, side, gomaOki, deadArea);
}),
new GungiHan(level),
);
gungi.setCurrentTurn(SIDE.WHITE);

await this._gungiRepository.save(gungi);

const event: CreateEvent = {
name: 'CreateEvent',
data: {
gungiId: gungi.id,
},
};
return presenter.present([event]);
}
}
Loading

0 comments on commit 734db1e

Please sign in to comment.