- 🎯 Project Goal
- 🐋 Docker
- ✅ How to test with Postman
- 📄 Use case diagram
- 🚩 App Routes
- ⏱ Sequence diagram
- 📐 Class diagram
- 🪄 Patterns used
- ⚙️ Technologies used
- 👨🏻💻 Authors
The backend is designed to offer complete management of combat sessions in "Dungeons & Dragons 5e", integrating directly with character created by players and npc sheets created by the master. These are created through the dedicated Flutter application, "SchedaDnD5e", which serves as the frontend for interaction with the system.
The requirements for this project are detailed in the following document, unfortunately in 🇮🇹 language.
The project is containerized using Docker and Docker Compose. In particular, the docker.api
file contains the instructions for containerizing the API server, while the docker.websocket
file outlines the process of containerizing the WebSocket server. Additionally, the docker-compose.yml
file contains also instructions for creating containers dedicated to the Postgres and Redis databases. These containers are based on public images sourced from Docker Hub.
- Build API container with image tag api_img:
docker build -t api_img -f .\Dockerfile.api .
- Build Websocket container with image tag websocket_img:
docker build -t websocket_img -f .\Dockerfile.websocket .
- Start all four containers:
docker-compose up -d
In compose up -d option, or detached mode, enables the creation and initiation of containers that run in the background, freeing up the terminal for other tasks.
These commands, executed sequentially, copy the contents of the src directory to the /usr/src/app/ directory of both the API and Websocket containers, and then restart the two containers to apply any changes to the copied files.
docker cp .\src\ api:/usr/src/app/;
docker cp .\src\ websocket:/usr/src/app/;
docker-compose restart api;
docker-compose restart websocket
Before testing, make sure you extract the following archive to the root directory. It contains unversioned files such as .env
, .dev.env
, websocket private key and firebase secrets.
The password to decrypt the archive can be requested to the authors.
The postman collection created for testing the routes is:
When started, the API server automatically seeds the PostgreSQL database with a test session. In this session there is a character controlled by a player and a monster controlled by the master.
To test the /attack
, /savingThrow
and /reaction
, which require the players to be connected, it is also needed to create two websockets in Postman, one for the master and one for the player.
Before creating the websocket, ensure that .env
file contains the row NODE_ENV= dev
. This, in conjunction with USE_JWT= false
in the .dev.env
file, allows token validation to be bypassed and allows the same player to handle multiple websocket connections.
The player websocket should be created as follows:
- The connection URL is
wss://localhost:8080/sessions/1
.
The master websocket should be created as follows:
- The connection URL is
wss://localhost:8080/sessions/1
. - In
Headers
section, add atoken
entry with valueHbE4YvSXSx6tbdBxB9Sn
, which is the identifier of the master player. This only works in development mode.
The player roles can be mapped as follows. Note that although the client must be authenticated via JWT to participate in the combat session, there is still a route that does not require authentication, namely the diceRoll/
route.
The API server endpoints are listed in the following table. Blank lines separate the routes following the semantic division of the previous use cases.
Type | Route | Parameters | Description |
---|---|---|---|
GET |
/sessions |
- | Provides the index of all sessions in which the authenticated user has the role of player or master. |
POST |
/sessions |
characters, npc, monsters, mapSize | Creates a new session. Returns the new session. |
GET |
/sessions/{sessionId} |
- | Returns all information from sessionId . |
DELETE |
/sessions/{sessionId} |
- | Deletes sessionId . |
PATCH |
/sessions/{sessionId}/start |
- | Starts sessionId . Its current status must be created . |
PATCH |
/sessions/{sessionId}/pause |
- | Pauses sessionId . Its current status must be ongoing . |
PATCH |
/sessions/{sessionId}/continue |
- | Resumes sessionId . Its current status must be paused . |
PATCH |
/sessions/{sessionId}/stop |
- | Ends sessionId . Its current status must be ongoing or paused . |
Type | Route | Parameters | Description |
---|---|---|---|
GET |
/sessions/{sessionId}/turn |
- | Provides the current turn of sessionId . |
PATCH |
/sessions/{sessionId}/turn/postpone |
entityId, predecessorEntityId | Postpones the turn of the entityId after the turn of the predecessorEntityId . |
PATCH |
/sessions/{sessionId}/turn/end |
entityId | Ends the turn of the entityId . Notifies the next playing entity. |
Type | Route | Parameters | Description |
---|---|---|---|
GET |
/diceRoll |
diceList, modifier? | Rolls the dice in the diceList and adds up any modifier . The diceList must be non empty. |
PATCH |
/sessions/{sessionId}/attack |
entityId, attackInfo, attackType | Causes attackerId to attack an entity. The attackType must contain the type of attack being made, which can be melee or enchantment. The attackInfo must contain the attempt dice roll. If this is greater than the target's AC, the attacker is asked to roll the damage dice. |
GET |
/sessions/{sessionId}/savingThrow |
entitiesId, difficultyClass, skill | Requests all the entitiesId to make a save roll on skill . The result is positive if greater than difficultyClass . |
PATCH |
/sessions/{sessionId}/effect |
entityId, effect | Assigns the effect to the entityId . If effect is null, the effects of the entities are deleted. |
PATCH |
/sessions/{sessionId}/reaction |
entityId | Enables the reaction for the entityId . Notifies it. |
Type | Route | Parameters | Description |
---|---|---|---|
PATCH |
/sessions/{sessionId}/entities |
entityType, entityInfo | Adds an entity to the sessionId . If the entityType is monster, entityInfo must contain all of its information. Otherwise it must only contain the uid. |
DELETE |
/sessions/{sessionId}/entities/{entityId} |
- | Removes entityId from sessionId . Fails if not found. |
GET |
/sessions/{sessionId}/entities/{entityId} |
- | Returns all the info from entityId . Fails if not found in sessionId . |
PATCH |
/sessions/{sessionId}/entities/{entityId} |
entityInfo | Updates the info of entityId . Fails if not found in sessionId . |
Type | Route | Parameters | Description |
---|---|---|---|
GET |
/sessions/{sessionId}/history |
actionType? | Returns the whole sessionId history. Filter it by actionType if provided. |
POST |
/sessions/{sessionId}/history |
message | Adds a message to the sessionId history. Notifies all players except the one who posted the message. |
The following patterns have been used in the development of solutions for the most critical aspects of the project.
The adoption of the Model-View-Controller architectural pattern makes it possible to decouple the management of the business logic, which is handled by the model, from the routing, which is handled by the controller.
However, the introduction of middleware in conjunction with the Chain of Responsability pattern allows the request validation phase to be decoupled from the response generation phase. In essence, the controller is only reached if all the middleware succeeds.
Middleware is natively supported by Express, and each can be thought of as a validator for a single aspect of the client request, independent of all others. This allows them to be reused across multiple routes in a very elegant way.
To generalize the middleware, higher order functions were used.
For example, body parameter type validation has been separated from the per-route middleware and delegated to the standalone checkMandadoryParams
middleware.
app.get('/diceRoll',
checkMandadoryParams(['diceList']),
checkParamsType({ diceList: ARRAY(ENUM(Dice)), modifier: INTEGER }),
...
checkParamsType
is a higher order function that takes as input an object where the keys are the body parameters and the value is the type checker function, and returns the middleware for that particular type check validation.
The type checker functions may be themselves higher order functions. For example, ARRAY
takes a type checker function as input and returns a type checker function that checks that the object provided is an array and then applies the latter to all the elements of the array.
export const ARRAY =
(next: (arg0: object) => boolean) =>
(obj: object) =>
Array.isArray(obj) && obj.every(it => next(it));
The factory method pattern has been used to centralize both client-side and server-side exceptions that may arise when handling the client request. It provides a lever of abstraction that helps the client code to ignore the actual error response generation, instead only requiring it to specify which ErrorProduct
to use.
This is implemented in the /src/error
directory.
In this project, three different data sources are used:
Firebase Firestore
: Stores the mobile app objects, such as players, characters, enchantments and npc.PostgreSQL
: Stores the combat session related information, such as sessions and monsters.Redis
: Implements an abstraction layer on top of the other two. Also caches the Firebase JWT with a short TTL to help reduce validation calls to the Firebase API (necessary because Firebase Auth uses a rolling public key, which is needed to validate the JWT signature).
As different objects are stored in different places, the repository pattern comes in very handy for decoupling from the location of the data.
Thanks to the repository pattern, client code can ignore not only which database the object is stored in, but also the caching policy, which is handled entirely by the repository itself.
This is implemented in the /src/repository
directory.
The websocket server makes extensive use of the observer design pattern using the RxJS library.
Callback functions are subscribed to Subject
objects, which are called repeatedly by the open
, message
and close
websocket events.
The timer
observable is used to prevent starvation when waiting for a player response through websocket. If the player answers or disconnects instead, the timer is interrupted using the takeUntil
function, which interrupts the timer emission before it reaches the abort Subject
.
This is implemented in the /src/websocket/websocket.ts
directory.
- Database: Sequelize with support for PostgreSQL for entity and session management.
- Data Modeling: Sequelize ORM for defining models and managing relationships between entities.
- Authentication: JWT to ensure secure and authorized access.
- Caching: Redis for cache management and performance improvement.
- API: RESTful API for communication between the frontend and backend, managed with Express.
- WebSocket: For real-time communication during combat sessions.
- Package Management: NPM for package and dependency management.
- Containerization: Docker for creating and managing isolated environments.
- Reactive Programming: RxJS for handling data streams and asynchronous events.
- HTTP requests: Axios for handling HTTP requests and interaction between the two API servers.
- Testing: Postman for creating API requests used for testing.
Name | GitHub | |
---|---|---|
Valerio Morelli | [email protected] | MrPio |
Enrico Maria Sardellini | [email protected] | Ems01 |
Federico Staffolani | [email protected] | fedeStaffo |