feat: Support CRUD functionality (#7)

The station search implementation only works via DB and is very slow. For a simple query, the answer takes about 1s or longer. That is 100 times slower than it should be. For now, however, this solution is adequate to achieve a first prototype that includes the core functionality.

feat: Add station search
This commit is contained in:
2024-01-06 14:06:43 +01:00
committed by GitHub
parent b5681181fb
commit 7fbd270019
166 changed files with 2039 additions and 482 deletions
+14
View File
@@ -0,0 +1,14 @@
plugins {
id("twomartens.spring-boot-base")
id("twomartens.kotlin")
kotlin("kapt")
}
dependencies {
implementation(project(":common"))
implementation(libs.spring.boot.mongo)
implementation(libs.mapstruct.base)
annotationProcessor(libs.mapstruct.processor)
kapt(libs.mapstruct.processor)
}
@@ -0,0 +1,6 @@
package de.twomartens.timetable.model.common
import de.twomartens.timetable.types.ZeroOrPositiveInteger
@JvmInline
value class CoachCapacity(val capacity: ZeroOrPositiveInteger)
@@ -0,0 +1,12 @@
package de.twomartens.timetable.model.common
import de.twomartens.timetable.types.NonEmptyString
@JvmInline
value class CountryCode(val countryCode: NonEmptyString) {
init {
require(countryCode.value.length == 2) {
"CountryCode must be only two characters long to conform to ISO 3166-1 alpha-2"
}
}
}
@@ -0,0 +1,12 @@
package de.twomartens.timetable.model.common
import de.twomartens.timetable.types.NonEmptyString
@JvmInline
value class DepotId private constructor(val id: String) {
companion object {
fun of(id: NonEmptyString): DepotId {
return DepotId(id.value)
}
}
}
@@ -0,0 +1,12 @@
package de.twomartens.timetable.model.common
import de.twomartens.timetable.types.NonEmptyString
@JvmInline
value class FormationId private constructor(val id: String) {
companion object {
fun of(id: NonEmptyString): FormationId {
return FormationId(id.value)
}
}
}
@@ -0,0 +1,6 @@
package de.twomartens.timetable.model.common
enum class LanguageCode {
de,
en
}
@@ -0,0 +1,14 @@
package de.twomartens.timetable.model.common
import de.twomartens.timetable.types.NonEmptyString
@JvmInline
value class Line private constructor(val name: String) {
companion object {
val EMPTY = Line("EMPTY")
fun of(name: NonEmptyString): Line {
return Line(name.value)
}
}
}
@@ -0,0 +1,6 @@
package de.twomartens.timetable.model.common
data class Platform(
var name: String,
var sections: MutableList<Section>
)
@@ -0,0 +1,12 @@
package de.twomartens.timetable.model.common
import de.twomartens.timetable.types.NonEmptyString
@JvmInline
value class PortalId private constructor(val id: String) {
companion object {
fun of(id: NonEmptyString): PortalId {
return PortalId(id.value)
}
}
}
@@ -0,0 +1,12 @@
package de.twomartens.timetable.model.common
import de.twomartens.timetable.types.PositiveInteger
@JvmInline
value class RotationId private constructor(val id: Int) {
companion object {
fun of(id: PositiveInteger): RotationId {
return RotationId(id.value)
}
}
}
@@ -0,0 +1,12 @@
package de.twomartens.timetable.model.common
import de.twomartens.timetable.types.NonEmptyString
@JvmInline
value class RouteId private constructor(val id: String) {
companion object {
fun of(id: NonEmptyString): RouteId {
return RouteId(id.value)
}
}
}
@@ -0,0 +1,12 @@
package de.twomartens.timetable.model.common
import de.twomartens.timetable.types.NonEmptyString
@JvmInline
value class Section private constructor(val value: String) {
companion object {
fun of(section: NonEmptyString): Section {
return Section(section.value)
}
}
}
@@ -0,0 +1,12 @@
package de.twomartens.timetable.model.common
import de.twomartens.timetable.types.NonEmptyString
@JvmInline
value class ServiceId private constructor(val value: String) {
companion object {
fun of(id: NonEmptyString): ServiceId {
return ServiceId(id.value)
}
}
}
@@ -0,0 +1,29 @@
package de.twomartens.timetable.model.common
import de.twomartens.timetable.types.NonEmptyString
class StationId private constructor(val value: String) {
companion object {
private val idPattern = Regex("^\\w{2}-(?<countryStationId>\\w.*)")
fun of(id: NonEmptyString): StationId {
require(idPattern.matches(id.value)) {
"StationId must start with two character country code, followed by a dash, " +
"and at least one word character"
}
return StationId(id.value)
}
}
val stationIdWithinCountry: String
get() {
val matchResult = idPattern.matchEntire(value)
val groupMatch = matchResult?.groups?.get("countryStationId")?.value
require(matchResult != null && groupMatch != null)
return groupMatch
}
}
@@ -0,0 +1,12 @@
package de.twomartens.timetable.model.common
import de.twomartens.timetable.types.NonEmptyString
@JvmInline
value class TimetableId private constructor(val value: String) {
companion object {
fun of(id: NonEmptyString): TimetableId {
return TimetableId(id.value)
}
}
}
@@ -0,0 +1,12 @@
package de.twomartens.timetable.model.common
import de.twomartens.timetable.types.PositiveInteger
@JvmInline
value class TrackId private constructor(val value: Int) {
companion object {
fun of(id: PositiveInteger): TrackId {
return TrackId(id.value)
}
}
}
@@ -0,0 +1,12 @@
package de.twomartens.timetable.model.common
import de.twomartens.timetable.types.NonEmptyString
@JvmInline
value class UserId private constructor(val value: String) {
companion object {
fun of(id: NonEmptyString): UserId {
return UserId(id.value)
}
}
}
@@ -0,0 +1,14 @@
package de.twomartens.timetable.model.db
import de.twomartens.timetable.model.common.DepotId
import de.twomartens.timetable.model.dto.Station
import de.twomartens.timetable.model.dto.Track
import de.twomartens.timetable.types.NonEmptyString
data class Depot(
val id: DepotId,
val name: NonEmptyString,
val nearestStation: Station,
val tracks: List<Track>,
val travelDurations: List<TravelDuration>
)
@@ -0,0 +1,33 @@
package de.twomartens.timetable.model.db
import de.twomartens.timetable.model.common.FormationId
import de.twomartens.timetable.model.common.UserId
import de.twomartens.timetable.types.NonEmptyString
import de.twomartens.timetable.types.ZeroOrPositiveInteger
import org.bson.types.ObjectId
import org.springframework.data.annotation.CreatedDate
import org.springframework.data.annotation.Id
import org.springframework.data.annotation.LastModifiedDate
import org.springframework.data.mongodb.core.index.CompoundIndex
import org.springframework.data.mongodb.core.mapping.Document
import java.time.Instant
@Document
@CompoundIndex(def = "{'userId': 1, 'formationId': 1}", unique = true)
data class Formation(
var userId: UserId,
var formationId: FormationId,
var name: NonEmptyString,
var trainSimWorldFormationId: FormationId?,
var formation: String,
var length: ZeroOrPositiveInteger
) {
@Id
var id: ObjectId = ObjectId()
@CreatedDate
lateinit var created: Instant
@LastModifiedDate
lateinit var lastModified: Instant
}
@@ -0,0 +1,12 @@
package de.twomartens.timetable.model.db
import de.twomartens.timetable.model.common.PortalId
import de.twomartens.timetable.model.dto.Station
import de.twomartens.timetable.types.NonEmptyString
data class Portal(
val id: PortalId,
val name: NonEmptyString,
val nearestStation: Station,
val travelDurations: List<TravelDuration>
)
@@ -0,0 +1,31 @@
package de.twomartens.timetable.model.db
import de.twomartens.timetable.model.common.CountryCode
import de.twomartens.timetable.model.common.Platform
import de.twomartens.timetable.model.common.StationId
import de.twomartens.timetable.types.NonEmptyString
import org.bson.types.ObjectId
import org.springframework.data.annotation.CreatedDate
import org.springframework.data.annotation.Id
import org.springframework.data.annotation.LastModifiedDate
import org.springframework.data.mongodb.core.index.CompoundIndex
import org.springframework.data.mongodb.core.mapping.Document
import java.time.Instant
@Document
@CompoundIndex(def = "{stationId: 1, countryCode: 1}", unique = true)
data class Station(
var stationId: StationId,
var countryCode: CountryCode,
var name: NonEmptyString,
var platforms: List<Platform>
) {
@Id
var id: ObjectId = ObjectId()
@CreatedDate
lateinit var created: Instant
@LastModifiedDate
lateinit var lastModified: Instant
}
@@ -0,0 +1,39 @@
package de.twomartens.timetable.model.db
import de.twomartens.timetable.model.common.RouteId
import de.twomartens.timetable.model.common.TimetableId
import de.twomartens.timetable.model.common.UserId
import de.twomartens.timetable.model.dto.TimetableState
import de.twomartens.timetable.types.NonEmptyString
import de.twomartens.timetable.types.ZeroOrPositiveInteger
import org.bson.types.ObjectId
import org.springframework.data.annotation.CreatedDate
import org.springframework.data.annotation.Id
import org.springframework.data.annotation.LastModifiedDate
import org.springframework.data.mongodb.core.index.CompoundIndex
import org.springframework.data.mongodb.core.mapping.Document
import java.time.Instant
import java.time.LocalDate
@Document
@CompoundIndex(def = "{userId: 1, timetableId: 1}", unique = true)
@CompoundIndex(def = "{userId: 1, routeId: 1}")
data class Timetable(
var userId: UserId,
var routeId: RouteId,
var routeName: String,
var timetableId: TimetableId,
var name: NonEmptyString,
var fetchDate: LocalDate,
var timetableState: TimetableState,
var numberOfServices: ZeroOrPositiveInteger
) {
@Id
var id: ObjectId = ObjectId()
@CreatedDate
lateinit var created: Instant
@LastModifiedDate
lateinit var lastModified: Instant
}
@@ -0,0 +1,8 @@
package de.twomartens.timetable.model.db
import de.twomartens.timetable.model.dto.Formation
data class TravelDuration(
val formation: Formation,
val time: Long
)
@@ -0,0 +1,35 @@
package de.twomartens.timetable.model.db
import de.twomartens.timetable.model.common.RouteId
import de.twomartens.timetable.model.common.UserId
import de.twomartens.timetable.model.dto.Country
import de.twomartens.timetable.model.dto.Station
import de.twomartens.timetable.types.NonEmptyString
import org.bson.types.ObjectId
import org.springframework.data.annotation.CreatedDate
import org.springframework.data.annotation.Id
import org.springframework.data.annotation.LastModifiedDate
import org.springframework.data.mongodb.core.index.CompoundIndex
import org.springframework.data.mongodb.core.mapping.Document
import java.time.Instant
@Document
@CompoundIndex(def = "{'userId': 1, 'name': 1}", unique = true)
data class TswRoute(
var userId: UserId,
var routeId: RouteId,
var name: NonEmptyString,
var country: Country,
var stations: List<Station>,
var depots: List<Depot>,
var portals: List<Portal>
) {
@Id
var id: ObjectId = ObjectId()
@CreatedDate
lateinit var created: Instant
@LastModifiedDate
lateinit var lastModified: Instant
}
@@ -0,0 +1,28 @@
package de.twomartens.timetable.model.db
import de.twomartens.timetable.model.common.UserId
import de.twomartens.timetable.types.Email
import de.twomartens.timetable.types.NonEmptyString
import org.bson.types.ObjectId
import org.springframework.data.annotation.CreatedDate
import org.springframework.data.annotation.Id
import org.springframework.data.annotation.LastModifiedDate
import org.springframework.data.mongodb.core.index.Indexed
import org.springframework.data.mongodb.core.mapping.Document
import java.time.Instant
@Document
data class User(
@Indexed(unique = true) var userId: UserId,
var name: NonEmptyString,
var email: Email
) {
@Id
var id: ObjectId = ObjectId()
@CreatedDate
lateinit var created: Instant
@LastModifiedDate
lateinit var lastModified: Instant
}
@@ -0,0 +1,14 @@
package de.twomartens.timetable.model.dto
import java.time.LocalTime
data class AdditionalService(
val id: String,
val line: String,
val formationId: String,
val direction: Direction,
val start: Station,
val destination: Station,
val serviceStartTime: LocalTime,
val stops: List<AdditionalServiceStop>
)
@@ -0,0 +1,11 @@
package de.twomartens.timetable.model.dto
import java.time.LocalTime
data class AdditionalServiceStop(
val stop: Station,
val arrivalTime: LocalTime,
val departureTime: LocalTime,
val platformAndSection: String,
val loading: Boolean
)
@@ -0,0 +1,6 @@
package de.twomartens.timetable.model.dto
data class Country(
val code: String,
val name: String
)
@@ -0,0 +1,9 @@
package de.twomartens.timetable.model.dto
data class Depot(
val id: String,
val name: String,
val nearestStation: Station,
val tracks: List<Track>,
val travelDurations: List<TravelDuration>
)
@@ -0,0 +1,6 @@
package de.twomartens.timetable.model.dto
enum class Direction {
LEFT,
RIGHT
}
@@ -0,0 +1,9 @@
package de.twomartens.timetable.model.dto
data class Formation(
val id: String,
val name: String,
val trainSimWorldFormation: Formation?,
val formation: String,
val length: Int
)
@@ -0,0 +1,8 @@
package de.twomartens.timetable.model.dto
data class Portal(
val id: String,
val name: String,
val nearestStation: Station,
val travelDurations: List<TravelDuration>
)
@@ -0,0 +1,11 @@
package de.twomartens.timetable.model.dto
import java.time.LocalTime
data class Rotation(
val id: String,
val formationId: String,
val firstServiceStartTime: LocalTime,
val lastServiceEndTime: LocalTime,
val startsInVault: Boolean
)
@@ -0,0 +1,11 @@
package de.twomartens.timetable.model.dto
data class ServiceFormationStage(
val id: String,
val line: String,
val direction: Direction,
val formation: String,
val formationReversed: Boolean,
val startStop: ServiceStop,
val destinationStop: ServiceStop
)
@@ -0,0 +1,14 @@
package de.twomartens.timetable.model.dto
data class ServiceLinkingStage(
val id: String,
val formationId: String,
val line: String,
val direction: Direction,
val formationReversed: Boolean,
val startStop: ServiceStop,
val fromDepotOrSidingAtStart: Boolean,
val destinationStop: ServiceStop,
val fromDepotOrSidingAtDestination: Boolean,
)
@@ -0,0 +1,15 @@
package de.twomartens.timetable.model.dto
import de.twomartens.timetable.model.common.Line
import de.twomartens.timetable.model.common.ServiceId
data class ServiceRotationStage(
val id: ServiceId,
val line: Line,
val originStop: String,
val virtualDestinations: List<String>,
val startingFrom: String,
val endingIn: String,
val serviceStartTime: String,
val stops: List<ServiceStop>
)
@@ -0,0 +1,9 @@
package de.twomartens.timetable.model.dto
data class ServiceStop(
val stop: Station,
val arrivalTime: String,
val departureTime: String,
val platform: String,
val section: String
)
@@ -0,0 +1,9 @@
package de.twomartens.timetable.model.dto
import de.twomartens.timetable.model.common.Platform
data class Station(
val id: String,
val name: String,
val platforms: List<Platform>
)
@@ -0,0 +1,13 @@
package de.twomartens.timetable.model.dto
import java.time.LocalDate
data class Timetable(
val id: String,
val name: String,
val routeId: String,
val routeName: String,
val date: LocalDate,
val state: TimetableState,
val numberOfServices: Int
)
@@ -0,0 +1,9 @@
package de.twomartens.timetable.model.dto
enum class TimetableState {
NEW,
PROCESSING,
ENTER_FORMATIONS,
LINK_SERVICES,
READY_FOR_USAGE
}
@@ -0,0 +1,7 @@
package de.twomartens.timetable.model.dto
data class Track(
val id: Int,
val name: String,
val capacity: Int
)
@@ -0,0 +1,6 @@
package de.twomartens.timetable.model.dto
data class TravelDuration(
val formation: Formation,
val time: Long
)
@@ -0,0 +1,13 @@
package de.twomartens.timetable.model.dto
data class TswRoute(
val id: String,
val name: String,
val country: Country,
val stations: List<Station>,
val firstStation: Station,
val lastStation: Station,
val numberOfStations: Int,
val depots: List<Depot>,
val portals: List<Portal>
)
@@ -0,0 +1,7 @@
package de.twomartens.timetable.model.dto
data class User(
val id: String,
val name: String,
val email: String
)
@@ -0,0 +1,36 @@
package de.twomartens.timetable.model.mapper
import de.twomartens.timetable.model.common.CountryCode
import de.twomartens.timetable.model.common.StationId
import de.twomartens.timetable.model.dto.Station
import de.twomartens.timetable.types.NonEmptyString
import org.mapstruct.*
@Mapper(
collectionMappingStrategy = CollectionMappingStrategy.ADDER_PREFERRED,
nullValueCheckStrategy = NullValueCheckStrategy.ALWAYS,
unmappedTargetPolicy = ReportingPolicy.IGNORE
)
interface StationMapper {
@Mapping(target = "id", ignore = true)
@Mapping(target = "created", ignore = true)
@Mapping(target = "lastModified", ignore = true)
fun mapToDB(countryCode: CountryCode, dto: Station): de.twomartens.timetable.model.db.Station {
return de.twomartens.timetable.model.db.Station(
StationId.of(NonEmptyString(countryCode.countryCode.value + "-" + dto.id)),
countryCode,
NonEmptyString(dto.name),
dto.platforms
)
}
fun mapStationsToDto(stations: List<de.twomartens.timetable.model.db.Station>): List<Station>
fun mapToDto(db: de.twomartens.timetable.model.db.Station): Station {
return Station(
db.stationId.stationIdWithinCountry,
db.name.value,
db.platforms
)
}
}
@@ -0,0 +1,30 @@
package de.twomartens.timetable.model.repository
import de.twomartens.timetable.model.common.StationId
import de.twomartens.timetable.model.db.Station
import org.bson.types.ObjectId
import org.springframework.data.mongodb.repository.Aggregation
import org.springframework.data.mongodb.repository.MongoRepository
interface StationRepository : MongoRepository<Station, ObjectId> {
fun findByCountryCodeAndStationId(countryCode: String, stationId: StationId): Station?
fun findAllByCountryCode(countryCode: String): List<Station>
@Aggregation(pipeline = [
"{\$search: { index: \"stations\", returnStoredSource: true, compound: {must: [{phrase: {query: ?0, path: \"countryCode\"}},{autocomplete: {query: ?1,path: \"name\",tokenOrder: \"sequential\"}}]}}}",
"{\$limit: 10}",
"{\$lookup: { from: \"station\", localField: \"_id\", foreignField: \"_id\", as: \"document\" }}",
"{\$unwind: \"\$document\"}",
"{\$replaceWith: \"\$document\"}"
])
fun findAllByCountryCodeAndNameContainingIgnoreCase(countryCode: String, name: String): List<Station>
@Aggregation(pipeline = [
"{\$search: { index: \"stations\", returnStoredSource: true, autocomplete: {query: ?0, path: \"name\",tokenOrder: \"sequential\"}}}",
"{\$limit: 10}",
"{\$lookup: { from: \"station\", localField: \"_id\", foreignField: \"_id\", as: \"document\" }}",
"{\$unwind: \"\$document\"}",
"{\$replaceWith: \"\$document\"}"
])
fun findAllByNameContainingIgnoreCase(name: String): List<Station>
}