You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

521 lines
19 KiB

This file contains invisible Unicode characters!

This file contains invisible Unicode characters that may be processed differently from what appears below. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to reveal hidden characters.

# PoKeMoNg
- [About](#about)
- [🗂DCM](#dcm)
- [`Trainer`](#trainer)
- [`Pokemong`](#pokemong)
- [`Move`](#move)
- [`Type`](#type)
- [🧬UML Class diagram](#uml-class-diagram)
- [🗺NoSQL Schema Versioning Strategy](#nosql-schema-versioning-strategy)
- [Schema Versioning Pattern](#schema-versioning-pattern)
- [Incremental Document Migration](#incremental-document-migration)
- [📇Indexes](#indexes)
- [`moves` collection](#moves-collection)
- [`pokemongs` collection](#pokemongs-collection)
- [`trainers` collection](#trainers-collection)
- [🐕🦺Services](#services)
- [🌺Special requests](#special-requests)
- [`Pokemong` by nickname](#pokemong-by-nickname)
- [`Pokemong` in date interval](#pokemong-in-date-interval)
- [🦚Aggregation pipeline](#aggregation-pipeline)
- [👔Some business rules](#some-business-rules)
- [`Move` CRUD cascade](#move-crud-cascade)
- [`Pokemong` CRUD cascade](#pokemong-crud-cascade)
- [`Trainer` CRUD cascade](#trainer-crud-cascade)
- [Prep steps](#prep-steps)
- [Java version](#java-version)
- [🔐Database connection](#database-connection)
- [Running the application in dev mode](#running-the-application-in-dev-mode)
- [API testing](#api-testing)
- [🧪Sample dataset](#sample-dataset)
- [🩺API testing tools](#api-testing-tools)
- [📱Front end](#front-end)
- [🏴SwaggerUI](#swaggerui)
- [Known limitations](#known-limitations)
- [🔀Types are left at the user's mercy](#types-are-left-at-the-users-mercy)
This is a [Quarkus](https://quarkus.io/) / [MongoDB](https://mongodb.com/) app for educational purposes.
Instructions are [here](https://clientserveur-courses.clubinfo-clermont.fr/Notation.html) for reference.
## About
A "Pokemong" is a playful term for a MongoDB pocket monster.
The application is developed using the Quarkus framework and uses MongoDB as its database.
This application is a RESTful service designed to emulate a basic `Pokemong` management system. It allows users to
perform
CRUD operations on `Pokemongs`, `Trainers`, and `Moves`.
### 🗂DCM
Let's cover the entities and relationships in this Data Concept Model:
#### `Trainer`
These are the individuals who capture and train `pokemongs`. They can engage in battles with other `trainers.`
* a `trainer` has fought between 0 and many `trainers`
* we will use *referencing*, since this is a reflexive relationship
* a `trainer` owns between 0 and many `pokemongs`
* we will use *referencing with denormalizing*, since `pokemongs` have lifecycles of their own
#### `Pokemong`
These are the creatures that `trainers` capture and train. They can be trained or wild.
* a `pokemong` is owned by 0 or 1 `trainer`
* we will use *referencing*, since `trainers` have lifecycles of their own, but no denormalizing, since no queries
need that
* a `pokemong` has 1 or 2 `types`
* we will use *embedding*, since `types` don't have lifecycles of their own
* a `pokemong` knows between 0 and 4 `moves`
* we will use *referencing with denormalizing*, since `moves` have lifecycles of their own
#### `Move`
These are the abilities or actions that a `pokemong` can perform. This covers the strategic aspects of battles, as
different `moves` can have different effects and powers depending on the type of the `pokemong` and the `move`.
* a `move` can be known by between 0 and zillions of `pokemongs`
* we will let `pokemongs` refer to `moves`, and not the other way around
* a `move` has 1 and only 1 `type`
* we will use *embedding*, since `types` don't have lifecycles of their own
#### `Type`
These define the elements or categories that a `pokemong` or a `move` can belong to.
* a `type` can define between 0 and zillions of `pokemongs`
* see [`Pokemong`](#pokemong)
* a `type` can define between 0 and zillions of `moves`
* see [`Move`](#move)
<img src="./docs/mcd.png" alt="Data Concept Model" title="Data Concept Model">
### 🧬UML Class diagram
Omitting some details, our entities look like this:
```mermaid
classDiagram
class Trainer {
+ id: ObjectId
+ name: string
+ dob: date
+ wins: int
+ losses: int
}
class Pokemong {
+ id: ObjectId
+ nickname: string?
+ dob: date
+ level: int
+ pokedexId: int
+ evoStage: int
+ evoTrack: PokemongName[]
}
class Move {
+ id: ObjectId
+ name: string
+ category: MoveCategoryName
+ power: int
+ accuracy: int
}
class Type {
+ id: ObjectId
+ name: TypeName
+ weakAgainst: TypeName[]
+ effectiveAgainst: TypeName[]
}
class TypeName {
<<enumeration>>
+ FIRE
+ WATER
+ ...
}
class PokemongName {
<<enumeration>>
+ BULBASAUR
+ IVYSAUR
+ ...
}
class MoveCategoryName {
<<enumeration>>
+ PHYSICAL
+ SPECIAL
+ STATUS
}
Trainer --> "0..*" Trainer: pastOpponents
Trainer --> "0..*" Pokemong: pokemongs
Pokemong --> "0..1" Trainer: trainer
Pokemong --> "0..4" Move: moveSet
Pokemong --> "1..2" Type: types
Move --> Type: type
Type ..> TypeName
Pokemong ..> PokemongName
Move ..> MoveCategoryName
```
### 🗺NoSQL Schema Versioning Strategy
This application uses MongoDB, a NoSQL database, which provides flexibility in our data model. While this flexibility
has
its advantages, it poses a unique challenge when we need to update our data model, specifically when we want to
introduce breaking changes in the existing schema.
We have adopted a schema versioning strategy to overcome this challenge and manage these changes efficiently.
#### Schema Versioning Pattern
Schema versioning is a pattern that involves tagging each document in a collection with a version number. This version
number corresponds to the schema of the document and is used to handle schema changes in the code that reads these
documents.
Each entity in our model extends a `GenericVersionedEntity` class, which includes a `schemaVersion` field. This field is
an integer that starts at 1 and is to be incremented by one with each schema change. Every change to the schema needs to
involve the schema version number being incremented.
#### Incremental Document Migration
When a document is read from the database, the version number in the document is checked. If the version number is less
than the current version, the document is updated to the current version, and the updated document is written back to
the database. This process effectively migrates the document to the current version.
In the example of the `Move` class, the codec's `decodeV1` method handles documents with a `schemaVersion` of less
than `2`. When it reads a document with this version, it updates the `schemaVersion` to `2`, and writes the updated
document back to the database.
```java
Move decodeV1(Document document){
// ...
// Increment the schemaVersion to the current version
move.setSchemaVersion(2);
// Save the updated Move object back to the database
moveRepository.persistOrUpdate(move);
// ...
}
```
This strategy allows for graceful schema evolution in a NoSQL environment. Instead of requiring all documents to be
migrated at once, which can be a time-consuming operation for large collections, it enables incremental document
migration. This approach also helps to avoid downtime during schema migration, as the application continues to function
correctly regardless of the document version. As documents are read, they are updated to the current schema version,
allowing the schema migration to happen gradually over time.
However, note that this strategy increases write operations to the database, which could affect application performance.
### 📇Indexes
Various indexes were created for fields that would often be queried in a dashboard situation. If there is an additional
reason, it will be specified below.
Unless otherwise specified, please consider indexes to be full, and ascending.
#### `moves` collection
In the front-end app, these are queried both in the detail screen and in the list screen.
* `name`
* `power`: Descending, because users are more likely to sort them in that order.
* `type`
#### `pokemongs` collection
* `nickname`: This field already has a dedicated endpoint for a nickname search filter.
* `dob`: Descending, because users are more likely to sort them in that order.
* `evoStage`: "Species" is calculated as `evoTrack[evoStage]`, and would often be queried.
* `evoTrack`: See `evoStage`. Yes, it's an array, but it's a one-to-few relationship.
* `trainer`: Partial index, to avoid indexing wild pokemongs there.
* `types`: It's an array, but it's a one-to-few relationship.
#### `trainers` collection
It was tempting to index `pastOpponents` and `pokemongs` in the `trainers` collection, but these arrays
could grow indefinitely, and the indexes may grow so large that they wouldn't fit in a server's RAM anymore.
* `name`
* `wins`: Descending, because users are more likely to sort them in that order for rankings.
* `losses`: Descending, because users are more likely to sort them in that order for rankings.
### 🐕🦺Services
Each entity (`Pokemong`, `Trainer`, `Move`) in the application has a corresponding service class. These service
classes are responsible for handling the business logic related to their respective entities. They interact with the
database through their associated repositories, performing CRUD operations.
All service classes inherit from a `GenericService` class, which provides the following methods:
* `addOne(T entity)`: Adds a new entity to the database, after validating it.
* `getOneById(String id)`: Retrieves a single entity from the database by its ID.
* `getAll()`: Retrieves all entities of a certain type from the database.
* `deleteOneById(String id)`: Deletes an entity from the database by its ID.
* `updateOne(T entity)`: Updates an existing entity in the database. This method is meant to be overridden in child
service classes to provide the specific update logic for each type of entity.
* `updateAll(List<T> entities)`: Updates all entities in a given list. Each entity is validated before updating.
These methods allow the application to perform all the basic CRUD operations on any type of entity. The specific logic
for each type of entity (like how to validate a `pokemong`, how to update a `move`, etc.) is provided in the child
service classes that inherit from `GenericService`.
Many business rules were applied, which can be browsed [here](#some-business-rules).
This diagram attempts to show the relationship between services in this API
```mermaid
classDiagram
class GenericService~T~ {
-GenericRepository~T~ repository
+setRepository(GenericRepository~T~ repository)
+addOne(T entity): T
+validateOne(T entity)
+getOneById(String id): T
+getAll(): List~T~
+deleteOneById(String id)
+updateOne(T entity): T
+updateAll(List~T~ entities)
}
class MoveService {
-MoveRepository moveRepository
-PokemongService pokemongService
+init()
+validateOne(Move move)
+getOneById(String id): Move
+getAll(): List~Move~
+deleteOneById(String id)
+updateOne(Move move): Move
+existsById(String moveId): boolean
-batchUpdatePokemongTrainers(Move move)
-migrateToV2(Move move): Move
}
class TrainerService {
-TrainerRepository trainerRepository
-PokemongService pokemongService
+init()
+addOne(Trainer trainer): Trainer
+validateOne(Trainer trainer)
+deleteOneById(String id)
+updateOne(Trainer trainer): Trainer
-transferNewlyArrivedTrainerPokemongs(...)
}
class PokemongService {
-PokemongRepository pokemongRepository
-MoveService moveService
-TrainerService trainerService
+init()
+addOne(Pokemong pokemong): Pokemong
+validateOne(Pokemong pokemong)
+deleteOneById(String id)
+updateOne(Pokemong pokemong): Pokemong
+existsById(String pokemongId): boolean
-updateTrainerPokemong(...)
+findByMove(String id): List~Pokemong~
+isEvoValid(String id, PokemongName species): boolean
+batchUpdatePokemongTrainers(...)
}
GenericService <|-- "T <- Move" MoveService
GenericService <|-- "T <- Trainer" TrainerService
GenericService <|-- "T <- Pokemong" PokemongService
```
### 🌺Special requests
This API goes a little bit beyond basic CRUD operations.
#### `Pokemong` by nickname
Using a MongoDB filter with a regex, `pokemongs` are searchable by nickname with the URL `/pokemong/nickname/{nickname}`
where `{nickname}` is a partial, case-insensitive search term.
#### `Pokemong` in date interval
Users can also use the route `pokemong/dob/{start-date}/{end-date}` to search for
`pokemongs` who where born within that interval (bounds included).
### 🦚Aggregation pipeline
Finally, the endpoint `pokemong/count-by-evo-stage` is provided, to get a mapping of evolution stages with
the number of `pokemongs`who achieved that evolution stage.
As an example of a potential output:
```json
[
{
"count": 15,
"evoStage": 0
},
{
"count": 4,
"evoStage": 1
},
{
"count": 5,
"evoStage": 2
}
]
```
### 👔Some business rules
#### `Move` CRUD cascade
* When you delete a `move`, it also gets deleted from any `pokemong`'s `moveSet`.
* Since `pokemongMove` is denormalized on the `name` field, that field also gets updated when a `move`'s `name` is
updated.
#### `Pokemong` CRUD cascade
* When a `pokemong` is created, the new `pokemong`'s information is also added to the `pokemongs` array of any
associated `trainer` documents.
* When a `pokemong` is deleted, the `pokemongs` array in the associated `trainer` documents also has that specific
`pokemong` removed.
* Since `trainerPokemong` is denormalized on the `nickname` and `species` fields, those fields also get updated when
a `pokemong`'s `nickname` is updated, or when a `pokemong` evolves.
#### `Trainer` CRUD cascade
* When a `trainer` is created, the new `trainer`'s information is also updated in the `trainer` field of any associated
`pokemong` documents. Since a `pokemong` can only belong to one `trainer` at a time, that may mean removing it from
one to give it to the other.
* When a `trainer` is deleted, the `trainer` field in the associated `pokemong` documents is also removed.
## Prep steps
### ♨Java version
This project is set up to use `Java 17`.
Your build will fail if the version of `Java` that your build tools are using does not match that.
<details><summary>💻 Run from command line</summary>
You should have `JDK 17` installed locally, and accessible to `Gradle`.
That may involve updating your `JAVA_HOME` and `Path` environment variables.
</details>
<details><summary>🛠️ Run from an IDE</summary>
If you're planning to run this app directly from an IDE like IntelliJ, make sure to update any `Gradle JVM` (or similar)
settings to use `JDK 17` for `Gradle` tasks
</details>
### 🔐Database connection
Note that the DB connection properties are not included -- your `src/main/resources/application.properties` should look
like this :
```properties
quarkus.mongodb.connection-string=mongodb+srv://<username>:<password>@<cluster>.<node>.mongodb.net
quarkus.mongodb.database=<database>
```
<details><summary>🏫 If you are the corrector</summary>
To be able to use this app, please update `application.properties` with the provided database secrets.
If none were provided, that was a mistake. Sorry. Please request them to the owner of this repo.
</details>
<details><summary>👥 If you are another user or developer</summary>
To be able to use this app, first create a MongoDB database, either locally or on
their [Atlas Cloud](https://cloud.mongodb.com/), then update `application.properties` with your database secrets.
You may want to look up the nice [MongoDB official documentation](https://www.mongodb.com/docs/) if you get stuck.
</details>
## Running the application in dev mode
You can run the application in dev mode using:
```shell script
./gradlew quarkusDev
```
## API testing
### 🧪Sample dataset
<details><summary>🏫 If you are the corrector</summary>
The database should already be populated with the sample dataset.
However, if you want to reload that dataset, please navigate to the root of this project in a terminal and run
the provided `load_data.sh` script.
If the script wasn't provided, that was a mistake. Sorry. Please request them to the owner of this repo, or follow the
alternate procedure below.
</details>
<details><summary>👥 If you are another user or developer</summary>
You can find a sample dataset at `data/sample-dataset/`. Each JSON file contains a collection.
For example, to load the `moves` collection into an existing MongoDB cluster, you may
use [MongoDB Shell ("mongosh")](https://www.mongodb.com/docs/mongodb-shell/) to run
```shell script
mongoimport --uri=mongodb+srv://<username>:<password>@<cluster>.<node>.mongodb.net/<databasename> --collection=moves --file=./data/sample-dataset/moves.json
```
You can then do the same, but changing `moves` for `pokemongs`, and then `trainers`
</details>
### 🩺API testing tools
You can use an API testing tool such as [Postman](https://www.postman.com/)
or [Insomnia](https://insomnia.rest/) to test this app.
If you use Postman, you can even import `data/postman_collection.json`, designed to work with the `🧪 Sample dataset`.
### 📱Front end
A corresponding [front-end app](https://github.com/draialexis/pokemong_app) comes into play for trying out this API.
⚠️ That only includes the `Move` entity, so [`Postman`](#api-testing-tools) seems like your best option at the moment.
### 🏴SwaggerUI
Thanks to this project's OpenAPI specs, you can explore the API in a lot of ways.
A popular choice is SwaggerUI -- after you run the app, just go to http://localhost:8080/q/swagger-ui and have fun.
⚠️ Swagger or Quarkus or SmallRye adds the field `id` to all request examples, but in fact
***you should NOT include id**
when you POST or UPDATE a new document.* The app takes care of it for you. Same thing for the field `species`
with `Pokemong` documents.
## Known limitations
### 🔀Types are left at the user's mercy
This API doesn't ensure that *a `Move` can't be both effective against a type and weak against that type*. It probably
should.
But then again, this API doesn't deal with types very much at all anyway. Users are free to create all sorts of weird
types within `pokemongs` and `moves`, such as a Pikachu with `GRASS` type effective against `ROCK`, who has an Ember
move with `GRASS` type weak against `ROCK` and effective against `FLYING`...