chore(persistence): actualized jdbc adapters according to new data structures

This commit is contained in:
ITQ
2026-05-20 02:51:09 +03:00
parent 75bbe27198
commit 54305f7d16
8 changed files with 408 additions and 16 deletions
@@ -18,6 +18,7 @@ class FilmLibraryRepository(
filmId = UUID.fromString(rs.getString("film_id")), filmId = UUID.fromString(rs.getString("film_id")),
comment = rs.getString("comment"), comment = rs.getString("comment"),
isViewed = rs.getBoolean("is_viewed"), isViewed = rs.getBoolean("is_viewed"),
watchedAt = rs.getTimestamp("watched_at")?.toLocalDateTime(),
) )
} }
@@ -26,26 +27,28 @@ class FilmLibraryRepository(
jdbc.update( jdbc.update(
""" """
UPDATE favorites UPDATE favorites
SET user_id = ?, film_id = ?, comment = ?, is_viewed = ? SET user_id = ?, film_id = ?, comment = ?, is_viewed = ?, watched_at = ?
WHERE id = ? WHERE id = ?
""".trimIndent(), """.trimIndent(),
filmLibrary.userId, filmLibrary.userId,
filmLibrary.filmId, filmLibrary.filmId,
filmLibrary.comment, filmLibrary.comment,
filmLibrary.isViewed, filmLibrary.isViewed,
filmLibrary.watchedAt,
filmLibrary.id, filmLibrary.id,
) )
if (updatedRows == 0) { if (updatedRows == 0) {
jdbc.update( jdbc.update(
""" """
INSERT INTO favorites (id, user_id, film_id, comment, is_viewed) INSERT INTO favorites (id, user_id, film_id, comment, is_viewed, watched_at)
VALUES (?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?)
""".trimIndent(), """.trimIndent(),
filmLibrary.id, filmLibrary.id,
filmLibrary.userId, filmLibrary.userId,
filmLibrary.filmId, filmLibrary.filmId,
filmLibrary.comment, filmLibrary.comment,
filmLibrary.isViewed, filmLibrary.isViewed,
filmLibrary.watchedAt,
) )
} }
return filmLibrary return filmLibrary
@@ -54,16 +57,30 @@ class FilmLibraryRepository(
override fun findById(id: UUID): FilmLibrary? { override fun findById(id: UUID): FilmLibrary? {
val entries = val entries =
jdbc.query( jdbc.query(
"SELECT id, user_id, film_id, comment, is_viewed FROM favorites WHERE id = ?", "SELECT id, user_id, film_id, comment, is_viewed, watched_at FROM favorites WHERE id = ?",
filmLibraryRowMapper, filmLibraryRowMapper,
id, id,
) )
return entries.firstOrNull() return entries.firstOrNull()
} }
override fun findByUserIdAndFilmId(
userId: UUID,
filmId: UUID,
): FilmLibrary? {
val entries =
jdbc.query(
"SELECT id, user_id, film_id, comment, is_viewed, watched_at FROM favorites WHERE user_id = ? AND film_id = ?",
filmLibraryRowMapper,
userId,
filmId,
)
return entries.firstOrNull()
}
override fun findAll(): List<FilmLibrary> = override fun findAll(): List<FilmLibrary> =
jdbc.query( jdbc.query(
"SELECT id, user_id, film_id, comment, is_viewed FROM favorites", "SELECT id, user_id, film_id, comment, is_viewed, watched_at FROM favorites",
filmLibraryRowMapper, filmLibraryRowMapper,
) )
@@ -0,0 +1,85 @@
package com.project.movienight.adapters.persistence.jdbc
import com.project.movienight.adapters.persistence.entity.FilmRatingEntity
import com.project.movienight.adapters.persistence.entity.toDomain
import com.project.movienight.adapters.persistence.entity.toEntity
import com.project.movienight.application.ports.output.FilmRatingRepositoryPort
import com.project.movienight.domain.model.FilmRating
import org.springframework.jdbc.core.JdbcTemplate
import org.springframework.stereotype.Repository
import java.sql.ResultSet
import java.time.LocalDateTime
import java.util.UUID
@Repository
class FilmRatingRepository(
private val jdbc: JdbcTemplate,
) : FilmRatingRepositoryPort {
private val rowMapper = { rs: ResultSet, _: Int ->
FilmRatingEntity(
id = UUID.fromString(rs.getString("id")),
userId = UUID.fromString(rs.getString("user_id")),
filmId = UUID.fromString(rs.getString("film_id")),
score = rs.getInt("score"),
note = rs.getString("note"),
createdAt = rs.getTimestamp("created_at").toLocalDateTime(),
updatedAt = rs.getTimestamp("updated_at").toLocalDateTime(),
)
}
override fun save(rating: FilmRating): FilmRating {
val entity = rating.toEntity()
val updatedRows =
jdbc.update(
"""
UPDATE film_ratings
SET score = ?, note = ?, updated_at = ?
WHERE user_id = ? AND film_id = ?
""".trimIndent(),
entity.score,
entity.note,
LocalDateTime.now(),
entity.userId,
entity.filmId,
)
if (updatedRows == 0) {
jdbc.update(
"""
INSERT INTO film_ratings (id, user_id, film_id, score, note, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?)
""".trimIndent(),
entity.id,
entity.userId,
entity.filmId,
entity.score,
entity.note,
entity.createdAt,
entity.updatedAt,
)
}
return rating
}
override fun findByUserId(userId: UUID): List<FilmRating> =
jdbc
.query(
"SELECT id, user_id, film_id, score, note, created_at, updated_at FROM film_ratings WHERE user_id = ?",
rowMapper,
userId,
).map { it.toDomain() }
override fun findByUserIdAndFilmId(
userId: UUID,
filmId: UUID,
): FilmRating? =
jdbc
.query(
"SELECT id, user_id, film_id, score, note, created_at, updated_at FROM film_ratings WHERE user_id = ? AND film_id = ?",
rowMapper,
userId,
filmId,
).firstOrNull()
?.toDomain()
}
@@ -1,6 +1,8 @@
package com.project.movienight.adapters.persistence.jdbc package com.project.movienight.adapters.persistence.jdbc
import com.project.movienight.adapters.persistence.jdbc.support.DelimitedValueCodec
import com.project.movienight.application.ports.output.FilmRepositoryPort import com.project.movienight.application.ports.output.FilmRepositoryPort
import com.project.movienight.domain.model.ContentType
import com.project.movienight.domain.model.Film import com.project.movienight.domain.model.Film
import org.springframework.jdbc.core.JdbcTemplate import org.springframework.jdbc.core.JdbcTemplate
import org.springframework.stereotype.Repository import org.springframework.stereotype.Repository
@@ -16,6 +18,21 @@ class FilmRepository(
id = UUID.fromString(rs.getString("id")), id = UUID.fromString(rs.getString("id")),
title = rs.getString("title"), title = rs.getString("title"),
description = rs.getString("description"), description = rs.getString("description"),
contentType =
runCatching {
ContentType.valueOf(
rs.getString("content_type"),
)
}.getOrDefault(ContentType.FILM),
releaseYear = rs.getObject("release_year")?.let { (it as Number).toInt() },
genres = DelimitedValueCodec.decodeList(rs.getString("genres")),
cast = DelimitedValueCodec.decodeList(rs.getString("cast_members")),
directors = DelimitedValueCodec.decodeList(rs.getString("directors")),
imdbRating = rs.getObject("imdb_rating")?.let { (it as Number).toDouble() },
platformRating = rs.getObject("platform_rating")?.let { (it as Number).toDouble() },
externalUrl = rs.getString("external_url"),
jellyfinItemId = rs.getString("jellyfin_item_id"),
jellyfinLibraryId = rs.getString("jellyfin_library_id"),
) )
} }
@@ -24,22 +41,42 @@ class FilmRepository(
jdbc.update( jdbc.update(
""" """
UPDATE films UPDATE films
SET title = ?, description = ? SET title = ?, description = ?, content_type = ?, release_year = ?, genres = ?, cast_members = ?, directors = ?, imdb_rating = ?, platform_rating = ?, external_url = ?, jellyfin_item_id = ?, jellyfin_library_id = ?
WHERE id = ? WHERE id = ?
""".trimIndent(), """.trimIndent(),
film.title, film.title,
film.description, film.description,
film.contentType.name,
film.releaseYear,
DelimitedValueCodec.encodeList(film.genres),
DelimitedValueCodec.encodeList(film.cast),
DelimitedValueCodec.encodeList(film.directors),
film.imdbRating,
film.platformRating,
film.externalUrl,
film.jellyfinItemId,
film.jellyfinLibraryId,
film.id, film.id,
) )
if (updatedRows == 0) { if (updatedRows == 0) {
jdbc.update( jdbc.update(
""" """
INSERT INTO films (id, title, description) INSERT INTO films (id, title, description, content_type, release_year, genres, cast_members, directors, imdb_rating, platform_rating, external_url, jellyfin_item_id, jellyfin_library_id)
VALUES (?, ?, ?) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""".trimIndent(), """.trimIndent(),
film.id, film.id,
film.title, film.title,
film.description, film.description,
film.contentType.name,
film.releaseYear,
DelimitedValueCodec.encodeList(film.genres),
DelimitedValueCodec.encodeList(film.cast),
DelimitedValueCodec.encodeList(film.directors),
film.imdbRating,
film.platformRating,
film.externalUrl,
film.jellyfinItemId,
film.jellyfinLibraryId,
) )
} }
return film return film
@@ -48,16 +85,36 @@ class FilmRepository(
override fun findById(id: UUID): Film? { override fun findById(id: UUID): Film? {
val films = val films =
jdbc.query( jdbc.query(
"SELECT id, title, description FROM films WHERE id = ?", "SELECT id, title, description, content_type, release_year, genres, cast_members, directors, imdb_rating, platform_rating, external_url, jellyfin_item_id, jellyfin_library_id FROM films WHERE id = ?",
filmRowMapper, filmRowMapper,
id, id,
) )
return films.firstOrNull() return films.firstOrNull()
} }
override fun findByJellyfinItemId(jellyfinItemId: String): Film? {
val films =
jdbc.query(
"SELECT id, title, description, content_type, release_year, genres, cast_members, directors, imdb_rating, platform_rating, external_url, jellyfin_item_id, jellyfin_library_id FROM films WHERE jellyfin_item_id = ?",
filmRowMapper,
jellyfinItemId,
)
return films.firstOrNull()
}
override fun findByJellyfinLibraryId(jellyfinLibraryId: String): Film? {
val films =
jdbc.query(
"SELECT id, title, description, content_type, release_year, genres, cast_members, directors, imdb_rating, platform_rating, external_url, jellyfin_item_id, jellyfin_library_id FROM films WHERE jellyfin_library_id = ?",
filmRowMapper,
jellyfinLibraryId,
)
return films.firstOrNull()
}
override fun findAll(): List<Film> = override fun findAll(): List<Film> =
jdbc.query( jdbc.query(
"SELECT id, title, description FROM films", "SELECT id, title, description, content_type, release_year, genres, cast_members, directors, imdb_rating, platform_rating, external_url, jellyfin_item_id, jellyfin_library_id FROM films",
filmRowMapper, filmRowMapper,
) )
@@ -0,0 +1,43 @@
package com.project.movienight.adapters.persistence.jdbc
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate
import org.springframework.stereotype.Repository
@Repository
class JellyfinEventRepository(
private val jdbc: NamedParameterJdbcTemplate,
) {
fun exists(eventId: String): Boolean {
val sql = "SELECT 1 FROM jellyfin_events WHERE event_id = :eventId"
val params = MapSqlParameterSource().addValue("eventId", eventId)
return jdbc.query(sql, params) { rs, _ -> rs.getInt(1) }.any()
}
fun save(
eventId: String,
serverId: String?,
eventType: String,
occurredAt: java.time.OffsetDateTime?,
jellyfinUserId: String?,
jellyfinItemId: String?,
payload: String?,
) {
val sql = """
INSERT INTO jellyfin_events(event_id, server_id, event_type, occurred_at, jellyfin_user_id, jellyfin_item_id, payload)
VALUES (:eventId, :serverId, :eventType, :occurredAt, :jellyfinUserId, :jellyfinItemId, cast(:payload as jsonb))
ON CONFLICT (event_id) DO NOTHING
""".trimIndent()
val params = MapSqlParameterSource()
.addValue("eventId", eventId)
.addValue("serverId", serverId)
.addValue("eventType", eventType)
.addValue("occurredAt", occurredAt)
.addValue("jellyfinUserId", jellyfinUserId)
.addValue("jellyfinItemId", jellyfinItemId)
.addValue("payload", payload)
jdbc.update(sql, params)
}
}
@@ -0,0 +1,75 @@
package com.project.movienight.adapters.persistence.jdbc
import com.project.movienight.adapters.persistence.entity.JellyfinSyncStateEntity
import com.project.movienight.adapters.persistence.entity.toDomain
import com.project.movienight.adapters.persistence.entity.toEntity
import com.project.movienight.application.ports.output.JellyfinSyncStateRepositoryPort
import com.project.movienight.domain.model.JellyfinSyncState
import org.springframework.jdbc.core.JdbcTemplate
import org.springframework.stereotype.Repository
import java.sql.ResultSet
import java.util.UUID
@Repository
class JellyfinSyncStateRepository(
private val jdbc: JdbcTemplate,
) : JellyfinSyncStateRepositoryPort {
private val rowMapper = { rs: ResultSet, _: Int ->
JellyfinSyncStateEntity(
userId = UUID.fromString(rs.getString("user_id")),
lastSyncedAt = rs.getTimestamp("last_synced_at")?.toLocalDateTime(),
lastSuccessfulSyncAt = rs.getTimestamp("last_successful_sync_at")?.toLocalDateTime(),
lastError = rs.getString("last_error"),
syncedItemCount = rs.getInt("synced_item_count"),
)
}
override fun save(state: JellyfinSyncState): JellyfinSyncState {
val entity = state.toEntity()
val updatedRows =
jdbc.update(
"""
UPDATE jellyfin_sync_state
SET last_synced_at = ?, last_successful_sync_at = ?, last_error = ?, synced_item_count = ?, updated_at = CURRENT_TIMESTAMP
WHERE user_id = ?
""".trimIndent(),
entity.lastSyncedAt,
entity.lastSuccessfulSyncAt,
entity.lastError,
entity.syncedItemCount,
entity.userId,
)
if (updatedRows == 0) {
jdbc.update(
"""
INSERT INTO jellyfin_sync_state (user_id, last_synced_at, last_successful_sync_at, last_error, synced_item_count)
VALUES (?, ?, ?, ?, ?)
""".trimIndent(),
entity.userId,
entity.lastSyncedAt,
entity.lastSuccessfulSyncAt,
entity.lastError,
entity.syncedItemCount,
)
}
return state
}
override fun findByUserId(userId: UUID): JellyfinSyncState? =
jdbc
.query(
"SELECT user_id, last_synced_at, last_successful_sync_at, last_error, synced_item_count FROM jellyfin_sync_state WHERE user_id = ?",
rowMapper,
userId,
).firstOrNull()
?.toDomain()
override fun findAll(): List<JellyfinSyncState> =
jdbc
.query(
"SELECT user_id, last_synced_at, last_successful_sync_at, last_error, synced_item_count FROM jellyfin_sync_state",
rowMapper,
).map { it.toDomain() }
}
@@ -0,0 +1,74 @@
package com.project.movienight.adapters.persistence.jdbc
import com.project.movienight.adapters.persistence.entity.UserPreferencesEntity
import com.project.movienight.adapters.persistence.entity.toDomain
import com.project.movienight.adapters.persistence.entity.toEntity
import com.project.movienight.application.ports.output.UserPreferencesRepositoryPort
import com.project.movienight.domain.model.UserPreferences
import org.springframework.jdbc.core.JdbcTemplate
import org.springframework.stereotype.Repository
import java.sql.ResultSet
import java.util.UUID
@Repository
class UserPreferencesRepository(
private val jdbc: JdbcTemplate,
) : UserPreferencesRepositoryPort {
private val rowMapper = { rs: ResultSet, _: Int ->
UserPreferencesEntity(
userId = UUID.fromString(rs.getString("user_id")),
weightedGenres = rs.getString("weighted_genres"),
plotTypes = rs.getString("plot_types"),
eras = rs.getString("eras"),
castAndDirectors = rs.getString("cast_and_directors"),
moods = rs.getString("moods"),
contentTypes = rs.getString("content_types"),
)
}
override fun save(preferences: UserPreferences): UserPreferences {
val entity = preferences.toEntity()
val updatedRows =
jdbc.update(
"""
UPDATE user_preferences
SET weighted_genres = ?, plot_types = ?, eras = ?, cast_and_directors = ?, moods = ?, content_types = ?
WHERE user_id = ?
""".trimIndent(),
entity.weightedGenres,
entity.plotTypes,
entity.eras,
entity.castAndDirectors,
entity.moods,
entity.contentTypes,
entity.userId,
)
if (updatedRows == 0) {
jdbc.update(
"""
INSERT INTO user_preferences (user_id, weighted_genres, plot_types, eras, cast_and_directors, moods, content_types)
VALUES (?, ?, ?, ?, ?, ?, ?)
""".trimIndent(),
entity.userId,
entity.weightedGenres,
entity.plotTypes,
entity.eras,
entity.castAndDirectors,
entity.moods,
entity.contentTypes,
)
}
return preferences
}
override fun findByUserId(userId: UUID): UserPreferences? =
jdbc
.query(
"SELECT user_id, weighted_genres, plot_types, eras, cast_and_directors, moods, content_types FROM user_preferences WHERE user_id = ?",
rowMapper,
userId,
).firstOrNull()
?.toDomain()
}
@@ -22,6 +22,7 @@ class UserRepository(
email = rs.getString("email"), email = rs.getString("email"),
provider = rs.getString("provider"), provider = rs.getString("provider"),
providerId = rs.getString("provider_id"), providerId = rs.getString("provider_id"),
jellyfinUserId = rs.getString("jellyfin_user_id"),
createdAt = rs.getTimestamp("created_at").toLocalDateTime(), createdAt = rs.getTimestamp("created_at").toLocalDateTime(),
) )
} }
@@ -32,26 +33,28 @@ class UserRepository(
jdbc.update( jdbc.update(
""" """
UPDATE users UPDATE users
SET name = ?, email = ?, provider = ?, provider_id = ? SET name = ?, email = ?, provider = ?, provider_id = ?, jellyfin_user_id = ?
WHERE id = ? WHERE id = ?
""".trimIndent(), """.trimIndent(),
entity.name, entity.name,
entity.email, entity.email,
entity.provider, entity.provider,
entity.providerId, entity.providerId,
entity.jellyfinUserId,
entity.id, entity.id,
) )
if (updatedRows == 0) { if (updatedRows == 0) {
jdbc.update( jdbc.update(
""" """
INSERT INTO users (id, name, email, provider, provider_id, created_at) INSERT INTO users (id, name, email, provider, provider_id, jellyfin_user_id, created_at)
VALUES (?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?, ?)
""".trimIndent(), """.trimIndent(),
entity.id, entity.id,
entity.name, entity.name,
entity.email, entity.email,
entity.provider, entity.provider,
entity.providerId, entity.providerId,
entity.jellyfinUserId,
entity.createdAt, entity.createdAt,
) )
} }
@@ -61,7 +64,7 @@ class UserRepository(
override fun findById(id: UUID): User? { override fun findById(id: UUID): User? {
val entities = val entities =
jdbc.query( jdbc.query(
"SELECT id, name, email, provider, provider_id, created_at FROM users WHERE id = ?", "SELECT id, name, email, provider, provider_id, jellyfin_user_id, created_at FROM users WHERE id = ?",
userEntityRowMapper, userEntityRowMapper,
id, id,
) )
@@ -71,7 +74,7 @@ class UserRepository(
override fun findAll(): List<User> = override fun findAll(): List<User> =
jdbc jdbc
.query( .query(
"SELECT id, name, email, provider, provider_id, created_at FROM users", "SELECT id, name, email, provider, provider_id, jellyfin_user_id, created_at FROM users",
userEntityRowMapper, userEntityRowMapper,
).map { it.toDomain() } ).map { it.toDomain() }
@@ -86,7 +89,7 @@ class UserRepository(
val entities = val entities =
jdbc.query( jdbc.query(
""" """
SELECT id, name, email, provider, provider_id, created_at FROM users SELECT id, name, email, provider, provider_id, jellyfin_user_id, created_at FROM users
WHERE provider = ? AND provider_id = ? WHERE provider = ? AND provider_id = ?
""".trimIndent(), """.trimIndent(),
userEntityRowMapper, userEntityRowMapper,
@@ -0,0 +1,38 @@
package com.project.movienight.adapters.persistence.jdbc.support
import java.net.URLDecoder
import java.net.URLEncoder
import java.nio.charset.StandardCharsets
object DelimitedValueCodec {
fun encodeList(values: List<String>): String = values.joinToString("|") { encode(it) }
fun decodeList(value: String?): List<String> =
value
?.takeIf { it.isNotBlank() }
?.split("|")
?.map { decode(it) }
?: emptyList()
fun encodeWeightedMap(values: Map<String, Int>): String =
values.entries.joinToString("|") { entry -> "${encode(entry.key)}:${entry.value}" }
fun decodeWeightedMap(value: String?): Map<String, Int> {
if (value.isNullOrBlank()) return emptyMap()
return value
.split("|")
.mapNotNull { pair ->
val parts = pair.split(":", limit = 2)
if (parts.size != 2) return@mapNotNull null
val key = decode(parts[0])
val weight = parts[1].toIntOrNull() ?: return@mapNotNull null
key to weight
}.toMap()
}
private fun encode(value: String): String = URLEncoder.encode(value, StandardCharsets.UTF_8)
private fun decode(value: String): String = URLDecoder.decode(value, StandardCharsets.UTF_8)
}