Skip to content

Latest commit

 

History

History
270 lines (184 loc) · 10.7 KB

README.md

File metadata and controls

270 lines (184 loc) · 10.7 KB

API

The API is built in Java with Spring Boot and PostgreSQL. Gradle is used as the build tool.

API Documentation

The API endpoints are documented with the OpenAPI Specification 3.0. To avoid dealing with a single large file, the parameter, response and schema components are documented in seperated files. During the deployment of the documentation to GitHub Pages, the files are bundled into a Swagger UI compliant file (see Bundle and deploy OpenAPI to GitHub Pages workflow). Thanks to a feature of the Redocly CLI, which is used for linting and bundling of the documentation, a public and an internal documentation can be generated, as paths for the public documentation can be excluded if the x-internal field is set to true.

Lint OpenAPI Documentation

In order to lint the OpenAPI documentation locally, as done by the Lint OpenAPI workflow, install the Redocly CLI globally via npm:

> npm install @redocly/cli@"^1.0.2" -g

and run:

> redocly lint api-internal --config ./api/openapi/redocly-cli-config.yml

Domain Model

The following model represents the domain layer:

Domain Model

Explanation and Further Constraints

VO stands for Value Object, which leads to an implementation as Embeddable in Spring.

Race

Unique(Season, name)

There cannot be several races with the same name in one season.

TeamOfSeason

A team of a season must have at least 2 drivers.

Unique(Season, Team)

There cannot be multiple instances of TeamOfSeason for the same team of a season.

DriverOfRace

As a driver can be in multiple teams of a season, DriverOfRace instances also save the team for that a driver has driven in a race.

Unique(Race, Driver)

There cannot be multiple instances of DriverOfRace for the same driver of a race.

Package Structure

Every entity from the domain model has its own package, which is dived into a domain and an application package. The first contains the entity and repository, while the latter is used for the application logic. The Controller, Application Service and DTO can be found here.

DTOs

To encapsulate domain objects, only DTOs are passed to the outside.

Mapping

The mapping between a domain object and the corresponding DTO is implemented with the MapStruct mapping framework in a *Mapper class of the application package (see e.g. SeasonMapper).

Validation

DTOs passed with a request are validated with Spring's Bean Validation. As the validation constraints are not always the same for create and update operations, the validation groups OnCreate and OnUpdate are used to specify when a constraint should apply, such as in the RaceDTO.

More information about validation with Spring can be found in this blog post.

Exceptions

Base Exception

ApiException acts as abstract base exception that is extended by all project specific exceptions.

Exception Handling

Every exception that is thrown is caught by the ApiExceptionHandler , which creates and returns a corresponding ApiExceptionInfo from the caught exception.

Query Parameters / Paging and Sorting

Paging is used to reduce network traffic if only a chunk of data is needed. General information about paging with Spring can be found in these blog posts on Reflectoring and HowToDoInJava.

Sort Property Handling

The sort query parameter can be used to sort entities by their properties. As a PropertyReferenceException is thrown, if a given sort property does not exist in the entity, properties that are named differently in the entity and the corresponding DTO must be mapped to each other.
This mapping is implemented in the SortPropertyMapper interface. It defines a getSortProperties and a map function.
The first returns a map of the related sort properties, where the keys are the property names in the DTO and the values are the corresponding property names in the entity. The concrete map is defined in the SortPropertyMapper implementation for the corresponding entity (see e.g. SeasonSortPropertyMapper).
The latter uses the map to create a valid Sort object: For each sort property of the given Sort object is checked whether it is a key in the sortProperties map. If this is the case, the sort property is replaced by the corresponding property name of the entity. If not, it is assumed that the property has the same name in the entity and the DTO, and it is checked whether the sort property is contained in the properties list (cf. e.g SeasonDTO) of the DTO and whether it is not an ID, as there is no point in sorting objects by their ID. If true, the property is left as it is. Otherwise, an ApiInvalidSortPropertyException is thrown.

Since the Sort object is encapsulated inside a Pageable object, the map function is called from the handleSortProperties functions of the SortPropertyHandler, which create a new Pageable instance that can be passed to repository methods.

Query Parameter Validation

Spring ignores invalid query parameters by default. For example, the GET Request /seasons?foo=bar would be interpreted as GET /seasons. As this behaviour is not intuitive, the checkIfParametersAreValid method of the QueryParameter interface checks if given query parameters are contained in the queryParameters set, defined for the corresponding entity and, if not, throws an ApiInvalidQueryParameterException.

Combination of Sort Property Handling and Query Parameter Validation

To check given query parameters and sort properties together the handleQueryParameters functions of the QueryParameterHandler are used.

ResponsePage

Instead of passing the complete page object as response body, only the properties totalElements, totalPages, pageNumber, pageSize and content of the page are returned as an instance of ResponsePage.

Environment Variables

SPRING_PROFILES_ACTIVE

Specifies the Spring Profile. Available options are prod and dev. Depending on which profile is used, either the configurations defined in application-prod.yml or application-dev.yml take effect.

If set to dev, starting the application will also start the postgres service, specified in the compose-dev.yml, via the Spring Docker Compose Support.

DEFAULT_PAGE_SIZE

Specifies the default pageSize used for paging of collections.

Default value: 25

MAX_PAGE_SIZE

Specifies the maximum allowed pageSize.

Default value: 100

prod specific Environment Variables

DB_URL

Specifies the PostgreSQL URL.

Default value: //postgres:5432/postgres

DB_USERNAME

Specifies the PostgreSQL user.

DB_PASSWORD

Specifies the password for the specified DB_USERNAME.

Docker

The Dockerfile, used to build the image of the API available on DockerHub, is based on suggestions of this Spring Guide. It makes use of Multi-stage builds and JAR layers to speed up subsequent builds. The Eclipse Temurin JRE image is used as base image.

Build Image

To build a container image from the Dockerfile, run:

> docker build -t <imageName>:<imageTag> --pull [--no-cache] .

Multi-platform Images

Multi-platform images can be built with builder instances whose driver type is docker-container. This command creates a builder with the name multi-platform-builder:

> docker buildx create --driver docker-container --name multi-platform-builder

To use the created builder to build an amd64 and arm64 linux image, execute:

> docker buildx --builder multi-platform-builder build \
  --push \
  -t <imageName>:<imageTag> \
  --platform linux/amd64,linux/arm64 \
  --pull \
  [--no-cache] .

Notice: The --push option will try to push the built images to DockerHub. To push them to a local registry, take a look at this blog post. Unfortunately, this seems to be the only easy way to extract the built images, as Docker currently does not support loading of multi-platform images in a stable way and therefore the --load option does not work (cf. GitHub Issue).

Environment Variables

See Environment Variables.

Notice: Set SPRING_PROFILES_ACTIVE to prod, as dev is meant to be used for local development only and does not work when running the application via Docker with the provided image.

Formatting

This project uses the palantir-java-format. To format files accordingly, execute:

> ./gradlew spotlessJavaApply

To check whether all files comply with the format, as done by the Spotless check workflow, run:

> ./gradlew spotlessJavaCheck