-
Notifications
You must be signed in to change notification settings - Fork 3
/
Router.kt
218 lines (191 loc) · 8.47 KB
/
Router.kt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
package nl.toefel.blog.exposed.rest
import io.javalin.Javalin
import io.javalin.http.Context
import nl.toefel.blog.exposed.db.Actors
import nl.toefel.blog.exposed.db.ActorsInMovies
import nl.toefel.blog.exposed.db.Movies
import nl.toefel.blog.exposed.dto.ActorDto
import nl.toefel.blog.exposed.dto.MovieActorCountDto
import nl.toefel.blog.exposed.dto.MovieSummary
import nl.toefel.blog.exposed.dto.MovieWithActorDto
import nl.toefel.blog.exposed.dto.MovieWithProducingActorDto
import org.jetbrains.exposed.sql.Join
import org.jetbrains.exposed.sql.JoinType
import org.jetbrains.exposed.sql.ResultRow
import org.jetbrains.exposed.sql.andWhere
import org.jetbrains.exposed.sql.count
import org.jetbrains.exposed.sql.deleteWhere
import org.jetbrains.exposed.sql.insert
import org.jetbrains.exposed.sql.select
import org.jetbrains.exposed.sql.selectAll
import org.jetbrains.exposed.sql.transactions.transaction
import org.joda.time.DateTime
import org.slf4j.Logger
import org.slf4j.LoggerFactory
/**
* Creates the webserver:
* 1. configures a request logger
* 2. enables CORS on all domains
* 3. configures available paths and their handlers
* 4. transforms database results to and from DTOs (client interface)
*/
class Router(val port: Int) {
private val logger: Logger = LoggerFactory.getLogger(Router::class.java)
val app = Javalin.create { cfg -> cfg.requestLogger(::logRequest).enableCorsForAllOrigins() }
.get("/actors", ::listAndFilterActors)
.post("/actors", ::createActor)
.get("/actors/:id", ::getActor)
.delete("/actors/:id", ::deleteActor)
.get("/movies", ::listMovies)
.get("/movies/:id", ::getMovie)
.get("/moviesActorCount", ::listMovieActorCount)
.get("/moviesWithActingProducers", ::listMoviesWithActingProducers)
private fun logRequest(ctx: Context, executionTimeMs: Float) =
logger.info("${ctx.method()} ${ctx.fullUrl()} status=${ctx.status()} durationMs=$executionTimeMs")
fun start(): Router {
app.start(port)
return this
}
fun printHints() {
logger.info("Navigate to: http://localhost:8080/actors")
logger.info("Navigate to: http://localhost:8080/actors?firstName=Angelina")
logger.info("Navigate to: http://localhost:8080/actors/1")
logger.info("Navigate to: http://localhost:8080/movies")
logger.info("Navigate to: http://localhost:8080/movies/2")
logger.info("Navigate to: http://localhost:8080/moviesActorCount")
logger.info("Navigate to: http://localhost:8080/moviesWithActingProducers")
}
fun listAndFilterActors(ctx: Context) {
val actorDtos = transaction {
// uses the connections initialized via Database.connect() in main!
val actorsQuery = Actors.selectAll()
// enable additional filters via query params if they are present (example: ?firstName=Angelina)
ctx.queryParam("id")?.let { actorsQuery.andWhere { Actors.id eq it.toInt() } }
ctx.queryParam("firstName")?.let { actorsQuery.andWhere { Actors.firstName eq it } }
ctx.queryParam("lastName")?.let { actorsQuery.andWhere { Actors.lastName eq it } }
ctx.queryParam("dateOfBirth")?.let { actorsQuery.andWhere { Actors.dateOfBirth eq DateTime.parse(it) } }
// map the database result rows to a DTO for the client
actorsQuery.map { mapToActorDto(it) }
}
ctx.json(actorDtos)
}
fun createActor(ctx: Context) {
val actorDto = ctx.bodyAsClass(ActorDto::class.java)
val insertedActorId = transaction {
Actors.insert {
it[firstName] = actorDto.firstName
it[lastName] = actorDto.lastName
it[dateOfBirth] = if (actorDto.dateOfBirth != null) DateTime.parse(actorDto.dateOfBirth) else null
} get Actors.id // fetches the auto generated ID
}
ctx.json(actorDto.copy(id = insertedActorId))
}
fun getActor(ctx: Context) {
val actorId = ctx.pathParam("id").toIntOrNull()
if (actorId == null) {
ctx.json("invalid id").status(400)
} else {
val actorDto = transaction {
Actors.select {Actors.id eq actorId}
.map { mapToActorDto(it) }
.firstOrNull()
}
if (actorDto == null) {
ctx.status(404)
} else {
ctx.json(actorDto).status(200)
}
}
}
fun deleteActor(ctx: Context) {
val actorId = ctx.pathParam("id").toIntOrNull()
if (actorId == null) {
ctx.json("invalid id").status(400)
} else {
val deletedCount = transaction { Actors.deleteWhere { Actors.id eq actorId } }
if (deletedCount == 0) {
ctx.json("no actor found with id $actorId").status(404)
} else {
ctx.json("actor with id $actorId deleted").status(200)
}
}
}
fun listMovies(ctx: Context) {
val allMoviesDtos = transaction {
val moviesQuery = Movies.selectAll()
// optional: enable additional filters via query params (example: ?name=Gladiator)
ctx.queryParam("id")?.let { moviesQuery.andWhere { Movies.id eq it.toInt() } }
ctx.queryParam("name")?.let { moviesQuery.andWhere { Movies.name eq it } }
ctx.queryParam("producerName")?.let { moviesQuery.andWhere { Movies.producerName eq it } }
ctx.queryParam("releaseDate")?.let { moviesQuery.andWhere { Movies.releaseDate eq DateTime.parse(it) } }
// map the result
moviesQuery.map { mapToMovieSummaryDto(it) }
}
ctx.json(allMoviesDtos)
}
fun getMovie(ctx: Context) {
val movieId = ctx.pathParam("id").toIntOrNull()
if (movieId == null) {
ctx.json("invalid id").status(400)
} else {
val movieDto: MovieWithActorDto? = transaction {
val movieOrNull = Movies.select { Movies.id eq movieId }.firstOrNull()
// if movie is not null, fetch the actors and map to the DTO
movieOrNull?.let { movie ->
val actors = Actors
.innerJoin(ActorsInMovies)
.innerJoin(Movies)
.slice(Actors.columns) // only select these columns to reduce data load
.select { Movies.id eq movieId }
.map { mapToActorDto(it) }
mapToMovieWithActorDto(movie, actors)
}
}
if (movieDto == null) {
ctx.json("no movie found with id $movieId").status(404)
} else {
ctx.json(movieDto).status(200)
}
}
}
fun listMovieActorCount(ctx: Context) {
val movieActorCounts = transaction {
Movies
.innerJoin(ActorsInMovies)
.innerJoin(Actors)
.slice(Movies.name, Actors.firstName.count())
.selectAll()
.groupBy(Movies.name)
.map { MovieActorCountDto(it[Movies.name], it[Actors.firstName.count()]) }
}
ctx.json(movieActorCounts).status(200)
}
/**
* Lists all movies that have a producer which is also known as an actor
*/
fun listMoviesWithActingProducers(ctx: Context) {
val moviesProducedByActors = transaction {
Join(Actors, Movies, JoinType.INNER, additionalConstraint = { Actors.firstName eq Movies.producerName })
.slice(Movies.name, Actors.firstName, Actors.lastName)
.selectAll()
.map { MovieWithProducingActorDto(it[Movies.name], "${it[Actors.firstName]} ${it[Actors.lastName]}") }
}
ctx.json(moviesProducedByActors).status(200)
}
}
fun mapToActorDto(it: ResultRow) = ActorDto(
id = it[Actors.id],
firstName = it[Actors.firstName],
lastName = it[Actors.lastName],
dateOfBirth = it[Actors.dateOfBirth]?.toString("yyyy-MM-dd"))
fun mapToMovieWithActorDto(it: ResultRow, actors: List<ActorDto>) = MovieWithActorDto(
id = it[Movies.id],
name = it[Movies.name],
producerName = it[Movies.producerName],
releaseDate = it[Movies.releaseDate].toString("yyyy-MM-dd"),
actors = actors)
fun mapToMovieSummaryDto(it: ResultRow) = MovieSummary(
id = it[Movies.id],
name = it[Movies.name],
producerName = it[Movies.producerName],
releaseDate = it[Movies.releaseDate].toString("yyyy-MM-dd"))