diff --git a/README.md b/README.md
index ac76ab5..7d95d34 100644
--- a/README.md
+++ b/README.md
@@ -2,15 +2,27 @@
- [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)
@@ -46,15 +58,21 @@ Let's cover the entities and relationships in this Data Concept Model:
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`
-* a `pokemong` knows between 0 and 4 `moves`,
+ * 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`
@@ -62,21 +80,25 @@ These are the abilities or actions that a `pokemong` can perform. This covers th
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)
-Looking at things from the point of view of entities, instead of relationships
-
### 🧬UML Class diagram
+Omitting some details, our entities look like this:
+
```mermaid
classDiagram
@@ -195,6 +217,39 @@ 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
@@ -203,39 +258,19 @@ 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 entities): Updates all entities in a given list. Each entity is validated before updating.
+* `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 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, so let's use just one for an example here: when a `trainer` gets updated, it can mean
-consequences for any number of `pokemongs`, as this commented code from inside `TrainerService.UpdateOne()` explains
+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`.
-```java
-// all old pokemongs who are not there anymore lose their trainer reference
-pokemongService.batchUpdatePokemongTrainers(
- oldPokemongs.stream()
- .filter(tp->!newPokemongs.contains(tp))
- .collect(Collectors.toSet()),
- null);
-// if this trainer gained a pokemong, that pokemong's ex-trainer if any needs to lose said pokemong
- transferNewlyArrivedTrainerPokemongs(oldPokemongs,newPokemongs);
-// all new pokemongs who were not there before gain this trainer's reference
- pokemongService.batchUpdatePokemongTrainers(
- newPokemongs.stream()
- .filter(tp->!oldPokemongs.contains(tp))
- .collect(Collectors.toSet()),
- existingTrainer.getId()
- );
-```
+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
@@ -337,6 +372,30 @@ As an example of a potential output:
]
```
+### 👔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
@@ -372,7 +431,7 @@ quarkus.mongodb.database=
🏫 If you are the corrector
-To be able to use this app, update `application.properties` with the provided database secrets.
+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.
diff --git a/data/postman_collection.json b/data/postman_collection.json
index 8ccb223..6376749 100644
--- a/data/postman_collection.json
+++ b/data/postman_collection.json
@@ -1,427 +1,447 @@
{
- "info": {
- "_postman_id": "11aa7a76-83a4-4b92-940f-528e29b66df8",
- "name": "PoKeMoNg",
- "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json",
- "_exporter_id": "25802734"
- },
- "item": [
- {
- "name": "move",
- "item": [
- {
- "name": "Create 1 move",
- "request": {
- "method": "POST",
- "header": [
- {
- "key": "Content-Type",
- "value": "application/json",
- "type": "text"
- }
- ],
- "body": {
- "mode": "raw",
- "raw": "{\r\n \"name\": \"Bubble beam\",\r\n \"category\": \"PHYSICAL\",\r\n \"power\": 10,\r\n \"accuracy\": 85,\r\n \"type\": \r\n {\r\n \"name\": \"WATER\",\r\n \"weakAgainst\": [\"GRASS\"],\r\n \"effectiveAgainst\": [\"FIRE\", \"GROUND\"]\r\n },\r\n \"schemaVersion\": 2\r\n}",
- "options": {
- "raw": {
- "language": "json"
- }
- }
- },
- "url": {
- "raw": "http://localhost:8080/move",
- "protocol": "http",
- "host": [
- "localhost"
- ],
- "port": "8080",
- "path": [
- "move"
- ]
- }
- },
- "response": []
- },
- {
- "name": "Get all moves",
- "request": {
- "method": "GET",
- "header": [],
- "url": {
- "raw": "http://localhost:8080/move",
- "protocol": "http",
- "host": [
- "localhost"
- ],
- "port": "8080",
- "path": [
- "move"
- ]
- }
- },
- "response": []
- },
- {
- "name": "Get 1 move",
- "request": {
- "method": "GET",
- "header": [],
- "url": {
- "raw": "http://localhost:8080/move/60a64f7eae945a6e60b0e917",
- "protocol": "http",
- "host": [
- "localhost"
- ],
- "port": "8080",
- "path": [
- "move",
- "60a64f7eae945a6e60b0e917"
- ]
- }
- },
- "response": []
- },
- {
- "name": "Update 1 move",
- "request": {
- "method": "PUT",
- "header": [],
- "body": {
- "mode": "raw",
- "raw": "{\r\n \"schemaVersion\": 2,\r\n \"name\": \"Ember UPDATED\",\r\n \"category\": \"SPECIAL\",\r\n \"power\": 40,\r\n \"accuracy\": 100,\r\n \"type\": {\r\n \"name\": \"FIRE\",\r\n \"weakAgainst\": [\r\n \"WATER\",\r\n \"GROUND\"\r\n ],\r\n \"effectiveAgainst\": [\r\n \"GRASS\"\r\n ]\r\n }\r\n}",
- "options": {
- "raw": {
- "language": "json"
- }
- }
- },
- "url": {
- "raw": "http://localhost:8080/move/60a64f7eae945a6e60b0e917",
- "protocol": "http",
- "host": [
- "localhost"
- ],
- "port": "8080",
- "path": [
- "move",
- "60a64f7eae945a6e60b0e917"
- ]
- }
- },
- "response": []
- },
- {
- "name": "Delete 1 move",
- "request": {
- "method": "DELETE",
- "header": [],
- "url": {
- "raw": "http://localhost:8080/move/60a64f7eae945a6e60b0e913",
- "protocol": "http",
- "host": [
- "localhost"
- ],
- "port": "8080",
- "path": [
- "move",
- "60a64f7eae945a6e60b0e913"
- ]
- }
- },
- "response": []
- }
- ]
- },
- {
- "name": "pokemong",
- "item": [
- {
- "name": "Create 1 pkmn",
- "protocolProfileBehavior": {
- "disabledSystemHeaders": {
- "content-type": true
- }
- },
- "request": {
- "method": "POST",
- "header": [
- {
- "key": "Content-Type",
- "value": "application/json"
- }
- ],
- "body": {
- "mode": "raw",
- "raw": "{\r\n \"nickname\": \"Blappity-bloop\",\r\n \"dob\": \"2023-05-07\",\r\n \"level\": 1,\r\n \"pokedexId\": 172,\r\n \"evoStage\": 1,\r\n \"evoTrack\": [\"PICHU\", \"PIKACHU\", \"RAICHU\"],\r\n \"types\": [\r\n {\r\n \"name\": \"ELECTRIC\",\r\n \"weakAgainst\": [\"GROUND\", \"ROCK\"],\r\n \"effectiveAgainst\": [\"WATER\", \"FLYING\"]\r\n }\r\n ],\r\n \"moveSet\": \r\n [\r\n {\r\n \"id\": \"60a64f7eae945a6e60b0e917\",\r\n \"name\": \"Ember\"\r\n }\r\n ],\r\n \"schemaVersion\": 1\r\n}"
- },
- "url": {
- "raw": "http://localhost:8080/pokemong",
- "protocol": "http",
- "host": [
- "localhost"
- ],
- "port": "8080",
- "path": [
- "pokemong"
- ]
- }
- },
- "response": []
- },
- {
- "name": "Get all pkmn",
- "request": {
- "method": "GET",
- "header": [],
- "url": {
- "raw": "http://localhost:8080/pokemong",
- "protocol": "http",
- "host": [
- "localhost"
- ],
- "port": "8080",
- "path": [
- "pokemong"
- ]
- }
- },
- "response": []
- },
- {
- "name": "Get 1 pkmn",
- "request": {
- "method": "GET",
- "header": [],
- "url": {
- "raw": "http://localhost:8080/pokemong/60a64f7eae945a6e60b0e911",
- "protocol": "http",
- "host": [
- "localhost"
- ],
- "port": "8080",
- "path": [
- "pokemong",
- "60a64f7eae945a6e60b0e911"
- ]
- }
- },
- "response": []
- },
- {
- "name": "Update 1 pkmn",
- "request": {
- "method": "PUT",
- "header": [
- {
- "key": "Content-Type",
- "value": "application/json"
- }
- ],
- "body": {
- "mode": "raw",
- "raw": "{\n \"schemaVersion\": 1,\n \"nickname\": \"Sparky UPDATED\",\n \"dob\": \"1994-02-18\",\n \"level\": 15,\n \"pokedexId\": 1,\n \"evoStage\": 1,\n \"evoTrack\": [\n \"BULBASAUR\",\n \"IVYSAUR\",\n \"VENUSAUR\"\n ],\n \"trainer\": \"60a64f7eae945a6e60b0e914\",\n \"types\": [\n {\n \"name\": \"GRASS\",\n \"weakAgainst\": [\n \"FIRE\"\n ],\n \"effectiveAgainst\": [\n \"WATER\",\n \"GROUND\"\n ]\n }\n ],\n \"moveSet\": [\n {\n \"id\": \"60a64f7eae945a6e60b0e912\",\n \"name\": \"Vine Whip\"\n }\n ]\n}"
- },
- "url": {
- "raw": "http://localhost:8080/pokemong/60a64f7eae945a6e60b0e911",
- "protocol": "http",
- "host": [
- "localhost"
- ],
- "port": "8080",
- "path": [
- "pokemong",
- "60a64f7eae945a6e60b0e911"
- ]
- }
- },
- "response": []
- },
- {
- "name": "Delete 1 pkmn",
- "request": {
- "method": "DELETE",
- "header": [],
- "url": {
- "raw": "http://localhost:8080/pokemong/60a64f7eae945a6e60b0e916",
- "protocol": "http",
- "host": [
- "localhost"
- ],
- "port": "8080",
- "path": [
- "pokemong",
- "60a64f7eae945a6e60b0e916"
- ]
- }
- },
- "response": []
- },
- {
- "name": "Get all pkmn by nickname",
- "request": {
- "method": "GET",
- "header": [],
- "url": {
- "raw": "http://localhost:8080/pokemong/nickname/sparky",
- "protocol": "http",
- "host": [
- "localhost"
- ],
- "port": "8080",
- "path": [
- "pokemong",
- "nickname",
- "sparky"
- ]
- }
- },
- "response": []
- },
- {
- "name": "Gat all pkmn by date interval",
- "request": {
- "method": "GET",
- "header": [],
- "url": {
- "raw": "http://localhost:8080/pokemong/dob/1995-01-01/1999-01-01",
- "protocol": "http",
- "host": [
- "localhost"
- ],
- "port": "8080",
- "path": [
- "pokemong",
- "dob",
- "1995-01-01",
- "1999-01-01"
- ]
- }
- },
- "response": []
- }
- ]
- },
- {
- "name": "trainer",
- "item": [
- {
- "name": "Create 1 trainer",
- "request": {
- "method": "POST",
- "header": [],
- "body": {
- "mode": "raw",
- "raw": "{\r\n \"schemaVersion\": 1,\r\n \"name\": \"Bloop\",\r\n \"dob\": \"1997-02-18\",\r\n \"wins\": 1,\r\n \"losses\": 50,\r\n \"pastOpponents\": [\r\n \"60a64f7eae945a6e60b0e915\"\r\n ],\r\n \"pokemongs\": [\r\n {\r\n \"id\": \"60a64f7eae945a6e60b0e911\",\r\n \"nickname\": \"Sparky UPDATED\",\r\n \"species\": \"IVYSAUR\"\r\n }\r\n ]\r\n}",
- "options": {
- "raw": {
- "language": "json"
- }
- }
- },
- "url": {
- "raw": "http://localhost:8080/trainer",
- "protocol": "http",
- "host": [
- "localhost"
- ],
- "port": "8080",
- "path": [
- "trainer"
- ]
- }
- },
- "response": []
- },
- {
- "name": "Get all trainers",
- "request": {
- "method": "GET",
- "header": [],
- "url": {
- "raw": "http://localhost:8080/trainer",
- "protocol": "http",
- "host": [
- "localhost"
- ],
- "port": "8080",
- "path": [
- "trainer"
- ]
- }
- },
- "response": []
- },
- {
- "name": "Get 1 trainer",
- "request": {
- "method": "GET",
- "header": [],
- "url": {
- "raw": "http://localhost:8080/trainer/60a64f7eae945a6e60b0e914",
- "protocol": "http",
- "host": [
- "localhost"
- ],
- "port": "8080",
- "path": [
- "trainer",
- "60a64f7eae945a6e60b0e914"
- ]
- }
- },
- "response": []
- },
- {
- "name": "Update 1 trainer",
- "request": {
- "method": "PUT",
- "header": [],
- "body": {
- "mode": "raw",
- "raw": "{\r\n \"schemaVersion\": 1,\r\n \"name\": \"Brock\",\r\n \"dob\": \"1994-02-18\",\r\n \"wins\": 60,\r\n \"losses\": 60,\r\n \"pastOpponents\": [\r\n \"60a64f7eae945a6e60b0e914\"\r\n ],\r\n \"pokemongs\": [\r\n {\r\n \"id\": \"60a64f7eae945a6e60b0e911\",\r\n \"nickname\": \"Sparky UPDATED\",\r\n \"species\": \"IVYSAUR\"\r\n }\r\n ]\r\n}",
- "options": {
- "raw": {
- "language": "json"
- }
- }
- },
- "url": {
- "raw": "http://localhost:8080/trainer/60a64f7eae945a6e60b0e915",
- "protocol": "http",
- "host": [
- "localhost"
- ],
- "port": "8080",
- "path": [
- "trainer",
- "60a64f7eae945a6e60b0e915"
- ]
- }
- },
- "response": []
- },
- {
- "name": "Delete 1 trainer",
- "request": {
- "method": "DELETE",
- "header": [],
- "url": {
- "raw": "http://localhost:8080/trainer/60a64f7eae945a6e60b0e914",
- "protocol": "http",
- "host": [
- "localhost"
- ],
- "port": "8080",
- "path": [
- "trainer",
- "60a64f7eae945a6e60b0e914"
- ]
- }
- },
- "response": []
- }
- ]
- }
- ]
+ "info": {
+ "_postman_id": "11aa7a76-83a4-4b92-940f-528e29b66df8",
+ "name": "PoKeMoNg",
+ "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json",
+ "_exporter_id": "25802734"
+ },
+ "item": [
+ {
+ "name": "move",
+ "item": [
+ {
+ "name": "Create 1 move",
+ "request": {
+ "method": "POST",
+ "header": [
+ {
+ "key": "Content-Type",
+ "value": "application/json",
+ "type": "text"
+ }
+ ],
+ "body": {
+ "mode": "raw",
+ "raw": "{\r\n \"name\": \"Bubble beam\",\r\n \"category\": \"PHYSICAL\",\r\n \"power\": 10,\r\n \"accuracy\": 85,\r\n \"type\": \r\n {\r\n \"name\": \"WATER\",\r\n \"weakAgainst\": [\"GRASS\"],\r\n \"effectiveAgainst\": [\"FIRE\", \"GROUND\"]\r\n },\r\n \"schemaVersion\": 2\r\n}",
+ "options": {
+ "raw": {
+ "language": "json"
+ }
+ }
+ },
+ "url": {
+ "raw": "http://localhost:8080/move",
+ "protocol": "http",
+ "host": [
+ "localhost"
+ ],
+ "port": "8080",
+ "path": [
+ "move"
+ ]
+ }
+ },
+ "response": []
+ },
+ {
+ "name": "Get all moves",
+ "request": {
+ "method": "GET",
+ "header": [],
+ "url": {
+ "raw": "http://localhost:8080/move",
+ "protocol": "http",
+ "host": [
+ "localhost"
+ ],
+ "port": "8080",
+ "path": [
+ "move"
+ ]
+ }
+ },
+ "response": []
+ },
+ {
+ "name": "Get 1 move",
+ "request": {
+ "method": "GET",
+ "header": [],
+ "url": {
+ "raw": "http://localhost:8080/move/60a64f7eae945a6e60b0e917",
+ "protocol": "http",
+ "host": [
+ "localhost"
+ ],
+ "port": "8080",
+ "path": [
+ "move",
+ "60a64f7eae945a6e60b0e917"
+ ]
+ }
+ },
+ "response": []
+ },
+ {
+ "name": "Update 1 move",
+ "request": {
+ "method": "PUT",
+ "header": [],
+ "body": {
+ "mode": "raw",
+ "raw": "{\r\n \"schemaVersion\": 2,\r\n \"name\": \"Ember UPDATED\",\r\n \"category\": \"SPECIAL\",\r\n \"power\": 40,\r\n \"accuracy\": 100,\r\n \"type\": {\r\n \"name\": \"FIRE\",\r\n \"weakAgainst\": [\r\n \"WATER\",\r\n \"GROUND\"\r\n ],\r\n \"effectiveAgainst\": [\r\n \"GRASS\"\r\n ]\r\n }\r\n}",
+ "options": {
+ "raw": {
+ "language": "json"
+ }
+ }
+ },
+ "url": {
+ "raw": "http://localhost:8080/move/60a64f7eae945a6e60b0e917",
+ "protocol": "http",
+ "host": [
+ "localhost"
+ ],
+ "port": "8080",
+ "path": [
+ "move",
+ "60a64f7eae945a6e60b0e917"
+ ]
+ }
+ },
+ "response": []
+ },
+ {
+ "name": "Delete 1 move",
+ "request": {
+ "method": "DELETE",
+ "header": [],
+ "url": {
+ "raw": "http://localhost:8080/move/60a64f7eae945a6e60b0e913",
+ "protocol": "http",
+ "host": [
+ "localhost"
+ ],
+ "port": "8080",
+ "path": [
+ "move",
+ "60a64f7eae945a6e60b0e913"
+ ]
+ }
+ },
+ "response": []
+ }
+ ]
+ },
+ {
+ "name": "pokemong",
+ "item": [
+ {
+ "name": "Create 1 pkmn",
+ "protocolProfileBehavior": {
+ "disabledSystemHeaders": {
+ "content-type": true
+ }
+ },
+ "request": {
+ "method": "POST",
+ "header": [
+ {
+ "key": "Content-Type",
+ "value": "application/json"
+ }
+ ],
+ "body": {
+ "mode": "raw",
+ "raw": "{\r\n \"nickname\": \"Blappity-bloop\",\r\n \"dob\": \"2023-05-07\",\r\n \"level\": 1,\r\n \"pokedexId\": 172,\r\n \"evoStage\": 1,\r\n \"evoTrack\": [\"PICHU\", \"PIKACHU\", \"RAICHU\"],\r\n \"types\": [\r\n {\r\n \"name\": \"ELECTRIC\",\r\n \"weakAgainst\": [\"GROUND\", \"ROCK\"],\r\n \"effectiveAgainst\": [\"WATER\", \"FLYING\"]\r\n }\r\n ],\r\n \"moveSet\": \r\n [\r\n {\r\n \"id\": \"60a64f7eae945a6e60b0e917\",\r\n \"name\": \"Ember\"\r\n }\r\n ],\r\n \"schemaVersion\": 1\r\n}"
+ },
+ "url": {
+ "raw": "http://localhost:8080/pokemong",
+ "protocol": "http",
+ "host": [
+ "localhost"
+ ],
+ "port": "8080",
+ "path": [
+ "pokemong"
+ ]
+ }
+ },
+ "response": []
+ },
+ {
+ "name": "Get all pkmn",
+ "request": {
+ "method": "GET",
+ "header": [],
+ "url": {
+ "raw": "http://localhost:8080/pokemong",
+ "protocol": "http",
+ "host": [
+ "localhost"
+ ],
+ "port": "8080",
+ "path": [
+ "pokemong"
+ ]
+ }
+ },
+ "response": []
+ },
+ {
+ "name": "Get 1 pkmn",
+ "request": {
+ "method": "GET",
+ "header": [],
+ "url": {
+ "raw": "http://localhost:8080/pokemong/60a64f7eae945a6e60b0e911",
+ "protocol": "http",
+ "host": [
+ "localhost"
+ ],
+ "port": "8080",
+ "path": [
+ "pokemong",
+ "60a64f7eae945a6e60b0e911"
+ ]
+ }
+ },
+ "response": []
+ },
+ {
+ "name": "Update 1 pkmn",
+ "request": {
+ "method": "PUT",
+ "header": [
+ {
+ "key": "Content-Type",
+ "value": "application/json"
+ }
+ ],
+ "body": {
+ "mode": "raw",
+ "raw": "{\n \"schemaVersion\": 1,\n \"nickname\": \"Sparky UPDATED\",\n \"dob\": \"1994-02-18\",\n \"level\": 15,\n \"pokedexId\": 1,\n \"evoStage\": 1,\n \"evoTrack\": [\n \"BULBASAUR\",\n \"IVYSAUR\",\n \"VENUSAUR\"\n ],\n \"trainer\": \"60a64f7eae945a6e60b0e914\",\n \"types\": [\n {\n \"name\": \"GRASS\",\n \"weakAgainst\": [\n \"FIRE\"\n ],\n \"effectiveAgainst\": [\n \"WATER\",\n \"GROUND\"\n ]\n }\n ],\n \"moveSet\": [\n {\n \"id\": \"60a64f7eae945a6e60b0e912\",\n \"name\": \"Vine Whip\"\n }\n ]\n}"
+ },
+ "url": {
+ "raw": "http://localhost:8080/pokemong/60a64f7eae945a6e60b0e911",
+ "protocol": "http",
+ "host": [
+ "localhost"
+ ],
+ "port": "8080",
+ "path": [
+ "pokemong",
+ "60a64f7eae945a6e60b0e911"
+ ]
+ }
+ },
+ "response": []
+ },
+ {
+ "name": "Delete 1 pkmn",
+ "request": {
+ "method": "DELETE",
+ "header": [],
+ "url": {
+ "raw": "http://localhost:8080/pokemong/60a64f7eae945a6e60b0e916",
+ "protocol": "http",
+ "host": [
+ "localhost"
+ ],
+ "port": "8080",
+ "path": [
+ "pokemong",
+ "60a64f7eae945a6e60b0e916"
+ ]
+ }
+ },
+ "response": []
+ },
+ {
+ "name": "Get many pkmn by nickname",
+ "request": {
+ "method": "GET",
+ "header": [],
+ "url": {
+ "raw": "http://localhost:8080/pokemong/nickname/sparky",
+ "protocol": "http",
+ "host": [
+ "localhost"
+ ],
+ "port": "8080",
+ "path": [
+ "pokemong",
+ "nickname",
+ "sparky"
+ ]
+ }
+ },
+ "response": []
+ },
+ {
+ "name": "Get many pkmn by date interval",
+ "request": {
+ "method": "GET",
+ "header": [],
+ "url": {
+ "raw": "http://localhost:8080/pokemong/dob/1995-01-01/1999-01-01",
+ "protocol": "http",
+ "host": [
+ "localhost"
+ ],
+ "port": "8080",
+ "path": [
+ "pokemong",
+ "dob",
+ "1995-01-01",
+ "1999-01-01"
+ ]
+ }
+ },
+ "response": []
+ },
+ {
+ "name": "Get mapping of all pkmn count by evo stage",
+ "request": {
+ "method": "GET",
+ "header": [],
+ "url": {
+ "raw": "http://localhost:8080/pokemong/count-by-evo-stage",
+ "protocol": "http",
+ "host": [
+ "localhost"
+ ],
+ "port": "8080",
+ "path": [
+ "pokemong",
+ "count-by-evo-stage"
+ ]
+ }
+ },
+ "response": []
+ }
+ ]
+ },
+ {
+ "name": "trainer",
+ "item": [
+ {
+ "name": "Create 1 trainer",
+ "request": {
+ "method": "POST",
+ "header": [],
+ "body": {
+ "mode": "raw",
+ "raw": "{\r\n \"schemaVersion\": 1,\r\n \"name\": \"Bloop\",\r\n \"dob\": \"1997-02-18\",\r\n \"wins\": 1,\r\n \"losses\": 50,\r\n \"pastOpponents\": [\r\n \"60a64f7eae945a6e60b0e915\"\r\n ],\r\n \"pokemongs\": [\r\n {\r\n \"id\": \"60a64f7eae945a6e60b0e911\",\r\n \"nickname\": \"Sparky UPDATED\",\r\n \"species\": \"IVYSAUR\"\r\n }\r\n ]\r\n}",
+ "options": {
+ "raw": {
+ "language": "json"
+ }
+ }
+ },
+ "url": {
+ "raw": "http://localhost:8080/trainer",
+ "protocol": "http",
+ "host": [
+ "localhost"
+ ],
+ "port": "8080",
+ "path": [
+ "trainer"
+ ]
+ }
+ },
+ "response": []
+ },
+ {
+ "name": "Get all trainers",
+ "request": {
+ "method": "GET",
+ "header": [],
+ "url": {
+ "raw": "http://localhost:8080/trainer",
+ "protocol": "http",
+ "host": [
+ "localhost"
+ ],
+ "port": "8080",
+ "path": [
+ "trainer"
+ ]
+ }
+ },
+ "response": []
+ },
+ {
+ "name": "Get 1 trainer",
+ "request": {
+ "method": "GET",
+ "header": [],
+ "url": {
+ "raw": "http://localhost:8080/trainer/60a64f7eae945a6e60b0e914",
+ "protocol": "http",
+ "host": [
+ "localhost"
+ ],
+ "port": "8080",
+ "path": [
+ "trainer",
+ "60a64f7eae945a6e60b0e914"
+ ]
+ }
+ },
+ "response": []
+ },
+ {
+ "name": "Update 1 trainer",
+ "request": {
+ "method": "PUT",
+ "header": [],
+ "body": {
+ "mode": "raw",
+ "raw": "{\r\n \"schemaVersion\": 1,\r\n \"name\": \"Brock\",\r\n \"dob\": \"1994-02-18\",\r\n \"wins\": 60,\r\n \"losses\": 60,\r\n \"pastOpponents\": [\r\n \"60a64f7eae945a6e60b0e914\"\r\n ],\r\n \"pokemongs\": [\r\n {\r\n \"id\": \"60a64f7eae945a6e60b0e911\",\r\n \"nickname\": \"Sparky UPDATED\",\r\n \"species\": \"IVYSAUR\"\r\n }\r\n ]\r\n}",
+ "options": {
+ "raw": {
+ "language": "json"
+ }
+ }
+ },
+ "url": {
+ "raw": "http://localhost:8080/trainer/60a64f7eae945a6e60b0e915",
+ "protocol": "http",
+ "host": [
+ "localhost"
+ ],
+ "port": "8080",
+ "path": [
+ "trainer",
+ "60a64f7eae945a6e60b0e915"
+ ]
+ }
+ },
+ "response": []
+ },
+ {
+ "name": "Delete 1 trainer",
+ "request": {
+ "method": "DELETE",
+ "header": [],
+ "url": {
+ "raw": "http://localhost:8080/trainer/60a64f7eae945a6e60b0e914",
+ "protocol": "http",
+ "host": [
+ "localhost"
+ ],
+ "port": "8080",
+ "path": [
+ "trainer",
+ "60a64f7eae945a6e60b0e914"
+ ]
+ }
+ },
+ "response": []
+ }
+ ]
+ }
+ ]
}
\ No newline at end of file
diff --git a/docs/DB.md b/docs/DB.md
deleted file mode 100644
index 6ced79a..0000000
--- a/docs/DB.md
+++ /dev/null
@@ -1,87 +0,0 @@
-# About the database schema
-
-## Collections
-
-### trainers collection
-
-* _id: ObjectId
-* name: string
- * (_indexed_: would often be queried in a dashboard situation)
-* dob: date
-* wins: int
-* losses: int
-* pastOpponents: array of ObjectId (references to other trainers)
- * (_indexed_: reflexivity would make deep queries quite slow, so it seems worthwhile)
-* pokemongs: array of ObjectId (references to owned pokemongs) + denormalizing on "nickname" and "species"
- * (_indexed_: to improve speed when querying non-denormalized fields)
-
-### pokemongs collection
-
-* _id: ObjectId
-* nickname: string?
-* dob: date
-* level: int
-* pokedexId: int
-* evoStage: int
- * (_indexed_: "species" is calculated as evoTrack[evoStage], and would often be queried)
-* evoTrack: array of strings (therefore "species" is evoTrack[evoStage], and "evoBase" is evoTrack[0])
- * (_indexed_: "species" is calculated as evoTrack[evoStage], and would be queried often)
-* trainer: ObjectId? (reference to a trainer) (but can be "wild" instead, if ref is null)
- * (_indexed_: could be queried often in a dashboard situation)
-* types: embedded type, or array of embedded types
- * (_indexed_: would often be queried in a dashboard situation)
-* moveSet: array of ObjectId (references to known moves) + denormalizing on "name"
-
-### moves collection
-
-* _id: ObjectId
-* name: string
- * (_indexed_: would often be queried in a dashboard situation)
-* category: string (can be "physical", "special", or "status")
-* power: int
- * (_indexed_: would often be used in sorts, in a dashboard situation)
-* accuracy: int
-* type: embedded type
- * (_indexed_: would often be queried in a dashboard situation)
-
-### types collection
-
-* _id: ObjectId
-* name: string
- * (_indexed_: would often be queried in a dashboard situation)
-* weakAgainst: array of strings (denormalized type names)
-* effectiveAgainst: array of strings (denormalized type names)
-
-## Relationships
-
-- Trainer
- - [x] trainers.pastOpponents: one-to-many and reflexive
- * => referencing
- - [x] trainers.pokemongs: one-to-many
- * => referencing + denormalizing on "nickname" and "species"
-- Pokemong
- - [x] pokemongs.trainer: many-to-one
- * => referencing
- - [x] pokemongs.types: one-to-few [1;2]
- * => embedding
- - [x] pokemongs.moveSet: one-to-few [0;4] but will also need to be queried independently
- * => referencing + denormalizing on "name"
-- Move
- - [x] moves.type: one-to-one [1;1]
- * => embedding
-- Type
- - [x] types.weakAgainst & types.effectiveAgainst: one-to-few, but reflexive
- * => denormalizing on "name"
-
-## Cascades
-
-- Pokemong
- - [x] delete ~> trainer.pokemongs
- - [x] update ~> trainer.pokemongs (denormalizing on "nickname" and "species")
- - [x] create ~> trainer.pokemongs
-- Trainer
- - [x] delete ~> pokemong.trainer
- - [x] create ~> pokemong.trainer
-- Move
- - [x] delete ~> pokemong.moveSet
- - [x] update ~> pokemong.moveSet (denormalizing on "name")
diff --git a/src/main/java/fr/uca/iut/Startup.java b/src/main/java/fr/uca/iut/Startup.java
new file mode 100644
index 0000000..87927f0
--- /dev/null
+++ b/src/main/java/fr/uca/iut/Startup.java
@@ -0,0 +1,46 @@
+package fr.uca.iut;
+
+import fr.uca.iut.entities.GenericEntity;
+import fr.uca.iut.repositories.GenericRepository;
+import fr.uca.iut.repositories.MoveRepository;
+import fr.uca.iut.repositories.PokemongRepository;
+import fr.uca.iut.repositories.TrainerRepository;
+import io.quarkus.runtime.StartupEvent;
+import jakarta.enterprise.context.ApplicationScoped;
+import jakarta.enterprise.event.Observes;
+import jakarta.inject.Inject;
+import org.bson.Document;
+
+@ApplicationScoped
+public class Startup {
+
+ @Inject
+ MoveRepository moveRepository;
+ @Inject
+ PokemongRepository pokemongRepository;
+ @Inject
+ TrainerRepository trainerRepository;
+
+ void onStart(@Observes StartupEvent ev) {
+ createIndex(moveRepository);
+ createIndex(pokemongRepository);
+ createIndex(trainerRepository);
+ }
+
+ private void createIndex(GenericRepository extends GenericEntity> repository) {
+ try {
+ repository.createIndexes();
+ printIndexes(repository);
+ } catch (Exception e) {
+ System.err.println("Error creating indexes for repository: " + repository.getClass());
+ e.printStackTrace();
+ }
+ }
+
+ private void printIndexes(GenericRepository extends GenericEntity> repository) {
+ System.out.println("indexes for " + repository.getClass());
+ for (Document index : repository.getCollection().listIndexes()) {
+ System.out.println(index.toJson());
+ }
+ }
+}
diff --git a/src/main/java/fr/uca/iut/repositories/GenericRepository.java b/src/main/java/fr/uca/iut/repositories/GenericRepository.java
index 1327e45..63ec20a 100644
--- a/src/main/java/fr/uca/iut/repositories/GenericRepository.java
+++ b/src/main/java/fr/uca/iut/repositories/GenericRepository.java
@@ -2,6 +2,7 @@ package fr.uca.iut.repositories;
import com.mongodb.client.MongoClient;
import com.mongodb.client.MongoCollection;
+import com.mongodb.client.MongoDatabase;
import com.mongodb.client.model.ReplaceOneModel;
import com.mongodb.client.model.ReplaceOptions;
import com.mongodb.client.model.WriteModel;
@@ -56,7 +57,7 @@ public abstract class GenericRepository {
*
* @return The MongoDB collection of entities of type T.
*/
- protected abstract MongoCollection getCollection();
+ public abstract MongoCollection getCollection();
/**
* Inserts an entity into the collection.
@@ -132,4 +133,14 @@ public abstract class GenericRepository {
Document query = new Document("_id", new ObjectId(id));
return getCollection().countDocuments(query) > 0;
}
+
+ /**
+ * @return the MongoDB database
+ */
+ @NotNull
+ public MongoDatabase getMongoDatabase() {
+ return mongoClient.getDatabase(DB_NAME);
+ }
+
+ public abstract void createIndexes();
}
diff --git a/src/main/java/fr/uca/iut/repositories/MoveRepository.java b/src/main/java/fr/uca/iut/repositories/MoveRepository.java
index a8726c3..98ce5db 100644
--- a/src/main/java/fr/uca/iut/repositories/MoveRepository.java
+++ b/src/main/java/fr/uca/iut/repositories/MoveRepository.java
@@ -3,6 +3,7 @@ package fr.uca.iut.repositories;
import com.mongodb.client.MongoClient;
import com.mongodb.client.MongoCollection;
import com.mongodb.client.MongoDatabase;
+import com.mongodb.client.model.Indexes;
import fr.uca.iut.entities.Move;
import jakarta.annotation.PostConstruct;
import jakarta.enterprise.context.ApplicationScoped;
@@ -24,8 +25,15 @@ public class MoveRepository extends GenericRepository {
}
@Override
- protected MongoCollection getCollection() {
+ public MongoCollection getCollection() {
MongoDatabase db = mongoClient.getDatabase(DB_NAME);
return db.getCollection(Move.COLLECTION_NAME, Move.class);
}
+
+ @Override
+ public void createIndexes() {
+ getCollection().createIndex(Indexes.ascending("name"));
+ getCollection().createIndex(Indexes.descending("power"));
+ getCollection().createIndex(Indexes.ascending("type"));
+ }
}
diff --git a/src/main/java/fr/uca/iut/repositories/PokemongRepository.java b/src/main/java/fr/uca/iut/repositories/PokemongRepository.java
index 409b29f..86af336 100644
--- a/src/main/java/fr/uca/iut/repositories/PokemongRepository.java
+++ b/src/main/java/fr/uca/iut/repositories/PokemongRepository.java
@@ -3,10 +3,7 @@ package fr.uca.iut.repositories;
import com.mongodb.client.MongoClient;
import com.mongodb.client.MongoCollection;
import com.mongodb.client.MongoDatabase;
-import com.mongodb.client.model.Accumulators;
-import com.mongodb.client.model.Aggregates;
-import com.mongodb.client.model.Filters;
-import com.mongodb.client.model.Projections;
+import com.mongodb.client.model.*;
import fr.uca.iut.entities.Pokemong;
import jakarta.annotation.PostConstruct;
import jakarta.enterprise.context.ApplicationScoped;
@@ -14,7 +11,6 @@ import jakarta.inject.Inject;
import org.bson.Document;
import org.bson.conversions.Bson;
import org.bson.types.ObjectId;
-import org.jetbrains.annotations.NotNull;
import java.time.LocalDate;
import java.time.ZoneId;
@@ -45,16 +41,11 @@ public class PokemongRepository extends GenericRepository {
}
@Override
- protected MongoCollection getCollection() {
+ public MongoCollection getCollection() {
MongoDatabase db = getMongoDatabase();
return db.getCollection(Pokemong.COLLECTION_NAME, Pokemong.class);
}
- @NotNull
- private MongoDatabase getMongoDatabase() {
- return mongoClient.getDatabase(DB_NAME);
- }
-
/**
* Fetches the list of Pokemong entities that have a nickname matching the provided nickname.
* The match is case-insensitive and ignores leading and trailing spaces.
@@ -108,8 +99,18 @@ public class PokemongRepository extends GenericRepository {
))
);
- MongoCollection collection = getMongoDatabase().getCollection(getCollection().getNamespace().getCollectionName());
- return collection.aggregate(pipeline, Document.class).into(new ArrayList<>());
+ return getCollection().aggregate(pipeline, Document.class).into(new ArrayList<>());
+ }
+
+ @Override
+ public void createIndexes() {
+ getCollection().createIndex(Indexes.ascending("nickname"));
+ getCollection().createIndex(Indexes.descending("dob"));
+ getCollection().createIndex(Indexes.ascending("evoStage"));
+ getCollection().createIndex(Indexes.ascending("evoTrack"));
+ getCollection().createIndex(Indexes.ascending("types"));
+ IndexOptions indexOptions = new IndexOptions().partialFilterExpression(Filters.exists("trainer", true));
+ getCollection().createIndex(Indexes.ascending("trainer"), indexOptions);
}
}
diff --git a/src/main/java/fr/uca/iut/repositories/TrainerRepository.java b/src/main/java/fr/uca/iut/repositories/TrainerRepository.java
index 30eb66e..c47a32f 100644
--- a/src/main/java/fr/uca/iut/repositories/TrainerRepository.java
+++ b/src/main/java/fr/uca/iut/repositories/TrainerRepository.java
@@ -3,6 +3,7 @@ package fr.uca.iut.repositories;
import com.mongodb.client.MongoClient;
import com.mongodb.client.MongoCollection;
import com.mongodb.client.MongoDatabase;
+import com.mongodb.client.model.Indexes;
import fr.uca.iut.entities.Trainer;
import jakarta.annotation.PostConstruct;
import jakarta.enterprise.context.ApplicationScoped;
@@ -24,8 +25,15 @@ public class TrainerRepository extends GenericRepository {
}
@Override
- protected MongoCollection getCollection() {
+ public MongoCollection getCollection() {
MongoDatabase db = mongoClient.getDatabase(DB_NAME);
return db.getCollection(Trainer.COLLECTION_NAME, Trainer.class);
}
+
+ @Override
+ public void createIndexes() {
+ getCollection().createIndex(Indexes.ascending("name"));
+ getCollection().createIndex(Indexes.descending("wins"));
+ getCollection().createIndex(Indexes.descending("losses"));
+ }
}