Giter Site home page Giter Site logo

holisticon / ranked Goto Github PK

View Code? Open in Web Editor NEW
22.0 12.0 0.0 5.86 MB

Tracking kicker results

License: Other

Kotlin 42.48% Shell 0.08% HTML 1.58% JavaScript 0.75% CSS 10.34% TypeScript 44.62% Dockerfile 0.14%
kotlin spring-boot axon-framework ranking cqrs-es

ranked's Introduction

Ranked

travis codecov

Application for tracking table-soccer results.

!! This repository is archived

Please note that we started the development of a new Ranked which is not based on this code, so the development of this is frozen and the repository is archived.

Running

Local

Use spring profile "local"

  • start the h2Application (from /test/h2)

  • start the RankedApplication (from backend/application)

  • create new Match using the form on http://localhost:8081/ or using Swagger.

Docker

  • run docker-compose up

Depending on your OS, you might have to replace localhost with your DOCKER_IP for all given urls.

Build

We use the maven-wrapper extension, to build everything, just run ./mvnw, which will default to mvn clean install using the maven version configured in .mvn/wrapper/maven-wrapper.properties.

If you want to produce docker images run ./mvnw dockerfile:build after the regular maven install ( extend with -f <DIR> if you need to rebuild a particular image). Docker builds produce images of the components

  • backend
  • frontend

If you want to produce docker images during the regular build, please run it eith Maven profile docker: (./mvnw -Pdocker) to clean, install and create the docker images.

Side Goals

  • Learning of SpringBoot with Kotlin
  • Learning of CQRS, Event Sourcing with AxonFramework
  • Learning ReactJS frontend technology

Ideas and Requirements

  • As a player, I want to enter the results of the match
  • As a player, I want to use chat bot for entering the data
  • As a player, I want to have a classic UI for entering data
  • As a player, I want to know my global ranking (elo-based)
  • As a player, I want to know my ranking over the last year

Development and architecture

We have several input channels and currently unknown algorithms to calculate on data and represent UI. In the same time, we know the target domain very well, so we foster domain-driven design and start with CQRS ES architecture style. Every target algorithm is a separate view on the domain, implemented by a view-projection on the event stream.

We use the following stack:

  • SpringBoot 2
  • Kotlin
  • AxonFramework 3

Decisions

Components and project structure

We came up with the following components and project structure for our application. The application is separated into frontend, backend, test and extensions modules.

Backend

The core CQRS application with all the business logic and even sourcing. Command and Query components are separate modules, but not separated applications, they share a common bus.

  • The backend/application is a component responsible for launching the entire application. It has dependencies to all other components and works as a packaging module for SpringBoot.
  • The backend/command component contains the core/command part of the CQRS application. It holds the aggregates and the commands and uses AxonFramework.
  • The backend/elo contains the business logic needed to do elo calculation
  • The backend/model component contains the value objects (Player, Team, etc) and the Event-Objects since they are shared between the command and the View-components.
  • The backend/properties component holding the properties definition for the entire project.
  • There are a bunch of View-Components, each responsible for a specific use case. The views have no persistence and act as tracking event processors on the stream of events stored in the event store of the application.
    • The backend/view/wall-view component is displaying the information about played matches. It is comparable with the facebook wall displaying news.
    • The backend/view/leaderboard-view component is calculating the best players and displays those.
    • The backend/view/player-view component encapsulates information about players available in the system.

Frontend

The frontend component contains the ReactJS single page application. Its build process is based on npm and webpack and is integrated into the Apache Maven component build. If you want to skip the frontend build, please use the following command:

    mvn clean install -P \!frontend

Then compile and start the frontend via mvn spring-boot:run in /frontend directory. It is then up and running under http://localhost:8080/.

Hot reloading is enabled via npm run start; which opens localhost:3000 automatically.

Test

  • The test/h2 is a h2 instance used during development. It provides a in-memory database which can be connected to using the tcp-socket.

Extensions

Library that provides kotlin-extension functions for Java, Spring and Axon. Expect this to be maintained and released separately.

CQRS runtime view

CQRS software design is different from classic OO/CRUD design with shared state. In order to depict this, the following diagramm has been created:

Sequence diagram

The diagram has been created using the https://sequencediagram.org/ and the diagram script is located in docs/sequencediagramm.org.txt.

API

There are two APIs of the application. The Command API is responsible for accepting user inputs (like recording new matches played). The View API is offering different views on application data (just following the CQRS pattern). Both APIs are RESTful APIs. If you want explore them, you can use Swagger, shipped as a part of the application. Just navigate to http://localhost:8081/swagger-ui.html and choose the API you want to see in the drop-down menu at the top-right of the screen.

The entire Command API is offered under /command resource. The entire View API is offered under /view resource.

Endpoints

A Zuul proxy is configured in a way, that it redirects the requests to the frontend (port 8080) to the backend (8081) for the following URLs

Example REST Requests

Currently, we support two ways of submitting the created match. In both cases, the match must contain information about the played sets. There are two possibilities to do so. Either you just provide the result of the set by specifying the number of goals shot by every team or you can provide a collection of goals with information which team scored and a timestamp.

Here is an example request for publishing the results only. Please note the type: result attribute:

  {
    "matchId": "4711",
    "teamRed": {
      "player1": {
        "value": "foo1"
      },
      "player2": {
        "value": "bar1"
      }
    },
    "teamBlue": {
      "player1": {
        "value": "zee1"
      },
      "player2": {
        "value": "onk1"
      }
    },
    "matchSets": [
      {
        "type": "result",
        "goalsRed": 6,
        "goalsBlue": 0,
        "offenseBlue": {
          "value": "onk1"
        },
        "offenseRed": {
          "value": "foo1"
        }
      },
      {
        "type": "result",
        "goalsRed": 6,
        "goalsBlue": 0,
        "offenseBlue": {
          "value": "onk1"
        },
        "offenseRed": {
          "value": "foo1"
        }
      }

    ],
    "tournamentId": "string",
    "startTime": [2018,1,19,20,49,48]
  }

Here is an example request for publishing the goals with timestamps. Please note the type: timestamp attribute:

  {
    "matchId": "4712",
    "teamRed": {
      "player1": {
        "value": "foo1"
      },
      "player2": {
        "value": "bar1"
      }
    },
    "teamBlue": {
      "player1": {
        "value": "zee1"
      },
      "player2": {
        "value": "onk1"
      }
    },
    "matchSets": [
      {
        "type": "timestamp",
        "goals": [
          {"first":"RED","second":[2018,1,19,20,49,48]},
          {"first":"BLUE","second":[2018,1,19,20,49,51]},
          {"first":"RED","second":[2018,1,19,20,50,15]},
          {"first":"BLUE","second":[2018,1,19,20,52,48]},
          {"first":"RED","second":[2018,1,19,20,53,17]},
          {"first":"BLUE","second":[2018,1,19,20,55,21]},
          {"first":"RED","second":[2018,1,19,20,59,1]},
          {"first":"RED","second":[2018,1,19,21,1,2]},
          {"first":"RED","second":[2018,1,19,21,2,57]}
        ],
        "offenseBlue": {
          "value": "onk1"
        },
        "offenseRed": {
          "value": "foo1"
        }
      },
      {
        "type": "timestamp",
        "goals": [
          {"first":"RED","second":[2018,1,19,20,49,48]},
          {"first":"BLUE","second":[2018,1,19,20,49,51]},
          {"first":"RED","second":[2018,1,19,20,50,15]},
          {"first":"BLUE","second":[2018,1,19,20,52,48]},
          {"first":"RED","second":[2018,1,19,20,53,17]},
          {"first":"BLUE","second":[2018,1,19,20,55,21]},
          {"first":"RED","second":[2018,1,19,20,59,1]},
          {"first":"RED","second":[2018,1,19,21,1,2]},
          {"first":"RED","second":[2018,1,19,21,2,57]}
        ],
        "offenseBlue": {
          "value": "onk1"
        },
        "offenseRed": {
          "value": "foo1"
        }
      }
    ],
    "tournamentId": "string",
    "startTime": [2018,1,19,20,49,48]
  }

Test framework

Though using plain junit/assertj unit tests would be possible, we want to try the kotlin way.

History

Wait... Last time I was here you spoke about implementing it in Scala. Right, the initial idea was to implement the application in Scala using the JEE stack. This idea has remained idea after the prototype implementation of basic aspects like persistence with JPA, some JEE Beans and controllers. After several years, we decided to try it again...

If you are still interested, check out the legacy-scala branch.

Team

  • Simon Zambrovski
  • Jan Galinski
  • Oliver Niebsch
  • Timo Gröger
  • Daniel Wegener (Scala version)

ranked's People

Contributors

danielwegener avatar hypery2k avatar jangalinski avatar oliverniebsch avatar oniebsch avatar sheldt avatar snyk-bot avatar srsp avatar timostuebing avatar zambrovski avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

ranked's Issues

UI: Scroll of upside down components wrong in iOS

Seems like a technical iOS issue. When selecting the players for team red, the selection is upside down (like the player icons). In this situation the scroll on iOS is wrong, because it isn't rotated.

setup jackson-databind for kotlin

Warning in spring-boot start:

2017-12-08 10:34:55.016  WARN 57594 --- [ost-startStop-1] o.s.h.c.j.Jackson2ObjectMapperBuilder    : For Jackson Kotlin classes support please add "com.fasterxml.jackson.module:jackson-module-kotlin" to the classpath

update kotlin to 1.2.31

somehow the classloading changed, we get test failures for bean validation because validation messages are not resolved.
With kotlin 1.2.10 this still works.

see also: #45

Cannot POST results

In the file frontend/src/main/js/pages:sendResults() I am sending the following POST to http://localhost:8080/command/match:

{ "teamRed":{ "player1":{ "value":"danielsteinhoefer" }, "player2":{ "value":"detlefvonderthuesen" } }, "teamBlue":{ "player1":{ "value":"lukastaake" }, "player2":{ "value":"leonfausten" } }, "matchSets":[ { "goalsRed":6, "goalsBlue":0, "offenseRed":{ "value":"danielsteinhoefer" }, "offenseBlue":{ "value":"lukastaake" } }, { "goalsRed":0, "goalsBlue":6, "offenseRed":{ "value":"detlefvonderthuesen" }, "offenseBlue":{ "value":"leonfausten" } } ], "type":"result" }

Unfortunately the validation fails with the following console output:

2018-01-26 16:11:48.431 WARN 65859 --- [nio-8081-exec-5] .w.s.m.s.DefaultHandlerExceptionResolver : Failed to read HTTP message: org.springframework.http.converter.HttpMessageNotReadableException: JSON parse error: Missing type id when trying to resolve subtype of [simple type, class de.holisticon.ranked.model.AbstractMatchSet]: missing type id property 'type' (for POJO property 'matchSets'); nested exception is com.fasterxml.jackson.databind.exc.InvalidTypeIdException: Missing type id when trying to resolve subtype of [simple type, class de.holisticon.ranked.model.AbstractMatchSet]: missing type id property 'type' (for POJO property 'matchSets') at [Source: (PushbackInputStream); line: 1, column: 298] (through reference chain: de.holisticon.ranked.command.api.CreateMatch["matchSets"]->java.util.ArrayList[0])

Remove MatchWinningSaga and adopt changes in the Aggregate

Currently Match Winning is an artificial construct to learn Sagas, but it feels like a smell, because it moves the logic of Match winning out of the match.

A better approach might be to calculate the match result inside of the Match Aggregate.

remove duplicate: model/ValidationMessages.properties

The update to kotlin 1.2.20 caused the bean validation test failed because the validation properties where not loaded from main/resources.

To temporarily fix this, the file was duplicated with 495b2ab

This should be removed again and fixed in a correct way.

Idea: maybe accessing resources in test/surefire changed?

use maven-oss-parent

replace the current spring boot parent with

    <groupId>io.toolisticon.maven</groupId>
    <artifactId>maven-oss-parent</artifactId>
    <version>0.4</version>
   <relativePath />   

UI: create match via rest

POST to

http://localhost:8081/command/createMatch (TODO: rename to /match)

to trigger axon creation of a match

view is empty after app-restart - replay tracking does not work

  • start h2-app, then ranked
  • create a match, verify its in wall-view and h2-domain
  • stop ranked - verify event still in h2
  • restart ranked -> view is empty (try manuall replay ... same result)
  • create match -> one match displayed in view, 2 events in h2

it really looks as if the replay (stop/delete token/start) does not work as expected ...

Move configuration into RankedProperties

Currently, single properties ale loaded via @Value Spring Annotation and then gathered to a single properties object in order to pass around.

In order to improve readability of the code, implement @ConfigurationProperties-way using Kotlin.

spring-data-rest with swagger

at least for the h2-app it would be nice to use rest-jpa repos and have them in swagger as well ... this is supposed to work wie springfox 2.7 but fails with spring boot 2 (so far)

warnings in maven build: Kotlin.Int

I see this

[WARNING] ...\ranked\command\src\main\kotlin\Configuration.kt: (47, 39) This class shouldn't be used in Kotlin. Use kotlin.Int instead.
[WARNING] ...\ranked\command\src\main\kotlin\Configuration.kt: (50, 41) This class shouldn't be used in Kotlin. Use kotlin.Int instead.
[WARNING] ...\ranked\command\src\main\kotlin\Configuration.kt: (53, 36) This class shouldn't be used in Kotlin. Use kotlin.Int instead.
[WARNING] ...\ranked\command\src\main\kotlin\Rest.kt: (32, 11) Variable 'result' is never used
[WARNING] ...\ranked\command\src\main\kotlin\aggregate\Player.kt: (30, 36) This class shouldn't be used in Kotlin. Use kotlin.Int instead.

Differentiate between type validation and domain validation

Validation of data is an important step towards prevention of errors and it should be executed as early as possible in the system. We use JSR330 Bean Validation to prevent wrong data from being entered into the system and put most of the data inside of the model classes, event classes and command classes.

Aparently, we have to be careful with validation constraints defined on the domain types, and separate them from rules defined through the domain aggregate logic.

Type constraints should be independent of the domain state and configuration - example, the score (=number of goals shot in the match) is a positive number or zero.

Domain constraints have a semantical meaning and may depend on domain configuration - example, the score to win a match at holisticon soccer is 6. It is wrong to put the validation of the score == 6 into domain type, because it is defined inside of our bounded context == aggregate, and is not defined in the domain.

P.S. if someone wants to define it inside the domain, it should be not ScoreToMinMatchSet, but something like HolisticonScoreToWinMatchSet - which makes a naming different and would be probably a bad domain type.

hikari/testdb - why do we have it - remove?

I saw on shutdown:

2017-11-10 22:15:04.375  INFO 50591 --- [       Thread-4] com.zaxxer.hikari.HikariDataSource       : testdb - Shutdown initiated...
2017-11-10 22:15:04.388  INFO 50591 --- [       Thread-4] com.zaxxer.hikari.HikariDataSource       : testdb - Shutdown completed.

testdb is the default in-mem db name, but we do not use it, we use localhost:9092/mem:ranked .

so why do we have testdb, how can we get rid of it, and is hikari (pooling) also used/usable for our tcp-db?

Story: Rankings ELO

As a user I want to be abel to see rankings of all players in a particular discipline sorted by rank.
wireframe-ranking

The rank should be calculated not solely based on elo-ranking, but similar to tennis ranking in e.g. WTA.

Adopt Query Handlers

AxonFrameowrk 3.1 introduced QueryHandlers as components that typically read data from the view models created by the Event listeners. Add them into application.

Story: Game Result

As a player I want to record the result of a game.
As an example of a two vs. two kicker best-of-three up to 6 points page is depicted.
wireframe-game

error on startup: primary key

2017-12-08 12:15:48.447 ERROR 61656 --- [           main] o.h.engine.jdbc.spi.SqlExceptionHelper   : Eindeutiger Index oder Primärschlüssel verletzt: "UK8S1F994P4LA2IPB13ME2XQM1W_INDEX_8 ON PUBLIC.DOMAIN_EVENT_ENTRY(AGGREGATE_IDENTIFIER, SEQUENCE_NUMBER) VALUES ('kermit', 0, 1)"
Unique index or primary key violation: "UK8S1F994P4LA2IPB13ME2XQM1W_INDEX_8 ON PUBLIC.DOMAIN_EVENT_ENTRY(AGGREGATE_IDENTIFIER, SEQUENCE_NUMBER) VALUES ('kermit', 0, 1)"; SQL statement:
insert into domain_event_entry (event_identifier, meta_data, payload, payload_revision, payload_type, time_stamp, aggregate_identifier, sequence_number, type, global_index) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) [23505-196]
2017-12-

Save sets in a heap

At the end of a match the frontend should send an array of MatchSets to the backend. Therefore we need to save the result of each set on a heap while playing a match.

Additionally we can fill the set counter with the current set number based on this heap.

Switch to Metadata for timestamps in events

Axon Frameowrk supplies metadata for fired events including the timestamp of event creation. Use this instead of polluting the command and event models with time information.

Spring Configuration and CQRS

After a small discussion with @jangalinski we identified the following problem. A system configuration provided by properties file (application.yml) should not change the system behavior. In general this is a problem, because the properties are exactly made for this: change the behaviour of the application. Having no "current" state in the application makes it difficult to handle. Here is an example:

Imagine we configure the property of number of sets to win a set to 6. The matches created with this value are validated against this number and the corresponding events are stored. If this number changes (because we consider the new rules and play till 10), the events created with value 6 become invalid.

To handle this issue, ths configration must be held inside of an aggregate. A config change should be considered as a system command, and the aggregate may reject it, or send the corresponding events to the components.

docker-compose: players are initialized twice

I noticed the following when I do docker-compose up:

  • players are created
  • Processors are shut down ( Stopping Player: Shutdown state set for Processor 'Player'. Awaiting termination...)
  • players are re-created.

this does not cause a problem or duplicate values, but doesn't look nice.

NPE when no sets are present

somehow, the sets are evaluated after the disjunct validation, which leads to NPE in the disjunct function

java.lang.NullPointerException: null
	at de.holisticon.ranked.command.api.CreateMatch$$EQbwrayu.disjunct(Commands.kt:53) ~[na:na]
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:1.8.0_131]
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:1.8.0_131]
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.8.0_131]
	at java.lang.reflect.Method.invoke(Method.java:498) ~[na:1.8.0_131]
	at org.springsource.loaded.ri.ReloadedTypeInvoker$2.invoke(ReloadedTypeInvoker.java:133) ~[springloaded-1.2.8.RELEASE.jar:1.2.8.RELEASE]
	at org.springsource.loaded.ri.ReflectiveInterceptor.jlrMethodInvoke(ReflectiveInterceptor.java:1462) ~[springloaded-1.2.8.RELEASE.jar:1.2.8.RELEASE]
	at org.springframework.expression.spel.support.ReflectiveMethodExecutor.execute(ReflectiveMethodExecutor.java:120) ~[spring-expression-5.0.1.RELEASE.jar:5.0.1.RELEASE]

fix apiInfo swagger/springfox

I had to remove the apiInfo part from the Docket because after updating to springfox 2.7.0 it failed

 // TODO: had to remove this because it fails with 2.7.0 ... api changed
//    .apiInfo(ApiInfo(
//      "Ranked Command API",
//      "Command API to record new match results in ranked.",
//      "1.0.0",
//      "None",
//      Contact("Holisticon Craftsmen", "https://www.holisticon.de", "[email protected]"),
//      "Revised BSD License", "https://github.com/holisticon/ranked/blob/master/LICENSE.txt"))

Contact is no longer valid at that point

user has image url

We want to display the user picture so the url to this image has to be stored when users are created.

UI: Allow to enter players after a match

If the players decide to start playing right away, there must be an option to specify the player names after a game in an intuitive manner.

Preferably the same UI, which is used to play the sets, is utilized.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.