大家好,我是 Brandy, 歡迎來到 Quarkus x Kotlin x Arrow KT 系列
在昨天的文章中,在 Quarkus 中利用了 OIDC 與 Access Token 可以換到使用者資訊。在修改資料時,常常會需要記錄 updater 是誰,這時不能直接由前端傳 userName 過來,應該是在 Quarkus 後端解 token 後才能確信是這個人。
所以本篇會講述如何把 updater 在新增、修改 FilmResource 時,記錄在 Mongodb 資料庫中。
如果要用原始碼比對差距,可以把 Day 15 branch 與 Day 21 branch 互相比較
修改 FilmEntity 增加 updater,此時其他相關程式碼會開始報錯,不過沒關係,compiler 會幫我們提醒/src/main/kotlin/tw/brandy/ironman/entity/FilmEntity.kt
data class FilmEntity @BsonCreator constructor(
....
@BsonProperty("updater") var updater: String
) : PanacheMongoEntityBase()
因為使用者資訊從 request 取得,所以在 add /updater 時, Request 所需要屬性不變。但傳給 Service 時需要有登入者資訊。所以新增一個 UpsertFilm 作為 request 時的 DTO,在 Kotlin 中有 data class ,所以不需吝嗇新增 Type, 而是利用多個 Type 清楚的知道每一次的 transform, 在 Design 時,也容易用 Data Type 表達
data class UpsertFilm(val title: String, val episodeID: Int, val director: String, val releaseDate: LocalDate)
data class Film(val title: String, val episodeID: Int, val director: String, val releaseDate: LocalDate, val updater: String)
等下在上傳時,會有這樣的一個 transform
val add = (UpsertFilm,User) -> Film
因為我們走 TDD 要,先來改 Test FilmResourceTest
,登入者是 alice 並且判斷新增後的 updater 是 alice.
@Test
@Order(1)
fun `test add`() {
Given {
contentType("application/json")
auth().oauth2(TestUtil.getAccessToken("alice"))
body(
"""{"title":"Spider Man","episodeID":100,"director":"Sam Raimi","releaseDate":"2002-04-29"}"""
)
} When {
post("/films")
} Then {
statusCode(200)
body("title", `is`("Spider Man"))
body("director", equalTo("Sam Raimi"))
body("updater", equalTo("alice"))
}
}
從 accessToken 得到 userName, 因為 token 可能為空或解不開,所以增加了兩個 Error typeAppError.kt
class idTokenEmpty : AppError()
class CreateUserFail(val e: Throwable) : AppError()
建立一個 UserSerivce.fromIdToken 方法利用 Either 來串接。這個 Service 不需依靠外部注入。可以用 object 直接宣告為 singleton class
UserService
object UserService {
fun fromAccessToken(accessToken: JsonWebToken?) = accessToken.toOption().toEither { AppError.idTokenEmpty() }.flatMap {
Either.catch {
User(it)
}.mapLeft { AppError.CreateUserFail(it) }
}
}
上傳與修改的接收端改成 UpsertFilm,並且整合登入者資訊
@Inject
var accessToken: JsonWebToken? = null
...
// (UpsertFilm -> (User) -> Film
suspend fun add(film: UpsertFilm) = UserService
.fromAccessToken(accessToken).map { user ->
Film(film.title, film.episodeID, film.director, film.releaseDate, user.userName)
}.flatMap { filmService.save(it) }
.toRestResponse()
Film(film.title, ... , user.userName)
會用到兩次,我們可以借助 IntelliJ Refactor 提成 function
並且因為在 Update 有相同的操作, IntelliJ 會幫你一併找到,並改成 function
@POST
suspend fun add(upsertFilm: UpsertFilm) = UserService
.fromAccessToken(accessToken).map { user ->
tofilm(upsertFilm, user)
}.flatMap { filmService.save(it) }
.toRestResponse()
@PUT
@Path("/{id}")
suspend fun update(upsertFilm: UpsertFilm) = UserService
.fromAccessToken(accessToken).map { user ->
tofilm(upsertFilm, user)
}.flatMap { filmService.update(it) }
.toRestResponse()
private fun tofilm(
film: UpsertFilm,
user: User
) = Film(film.title, film.episodeID, film.director, film.releaseDate, user.userName)
最後 serivce 層與 test 會經過必要的修改,compiler 都會提醒我們。改完後可以看到我們成功的跑過所有測試了!