feat: Use tag name directly instead of ID

This commit is contained in:
oSumAtrIX 2024-11-02 00:24:31 +01:00
parent e8dfefe6ae
commit fc40427fba
No known key found for this signature in database
GPG Key ID: A9B3094ACDB604B4
5 changed files with 48 additions and 48 deletions

View File

@ -6,7 +6,7 @@ import app.revanced.api.configuration.schema.ApiResponseAnnouncement
import app.revanced.api.configuration.schema.ApiResponseAnnouncementId import app.revanced.api.configuration.schema.ApiResponseAnnouncementId
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import kotlinx.datetime.* import kotlinx.datetime.toKotlinLocalDateTime
import org.jetbrains.exposed.dao.IntEntity import org.jetbrains.exposed.dao.IntEntity
import org.jetbrains.exposed.dao.IntEntityClass import org.jetbrains.exposed.dao.IntEntityClass
import org.jetbrains.exposed.dao.id.EntityID import org.jetbrains.exposed.dao.id.EntityID
@ -20,7 +20,7 @@ import java.time.LocalDateTime
internal class AnnouncementRepository(private val database: Database) { internal class AnnouncementRepository(private val database: Database) {
// This is better than doing a maxByOrNull { it.id } on every request. // This is better than doing a maxByOrNull { it.id } on every request.
private var latestAnnouncement: Announcement? = null private var latestAnnouncement: Announcement? = null
private val latestAnnouncementByTag = mutableMapOf<Int, Announcement>() private val latestAnnouncementByTag = mutableMapOf<String, Announcement>()
init { init {
runBlocking { runBlocking {
@ -40,22 +40,23 @@ internal class AnnouncementRepository(private val database: Database) {
private fun initializeLatestAnnouncements() { private fun initializeLatestAnnouncements() {
latestAnnouncement = Announcement.all().orderBy(Announcements.id to SortOrder.DESC).firstOrNull() latestAnnouncement = Announcement.all().orderBy(Announcements.id to SortOrder.DESC).firstOrNull()
Tag.all().map { it.id.value }.forEach(::updateLatestAnnouncementForTag) Tag.all().map { it.name }.forEach(::updateLatestAnnouncementForTag)
} }
private fun updateLatestAnnouncement(new: Announcement) { private fun updateLatestAnnouncement(new: Announcement) {
if (latestAnnouncement == null || latestAnnouncement!!.id.value <= new.id.value) { if (latestAnnouncement == null || latestAnnouncement!!.id.value <= new.id.value) {
latestAnnouncement = new latestAnnouncement = new
new.tags.forEach { tag -> latestAnnouncementByTag[tag.id.value] = new } new.tags.forEach { tag -> latestAnnouncementByTag[tag.name] = new }
} }
} }
private fun updateLatestAnnouncementForTag(tag: Int) { private fun updateLatestAnnouncementForTag(tag: String) {
val latestAnnouncementForTag = AnnouncementTags.select(AnnouncementTags.announcement) val latestAnnouncementForTag = Tags.innerJoin(AnnouncementTags)
.where { AnnouncementTags.tag eq tag } .select(AnnouncementTags.announcement)
.map { it[AnnouncementTags.announcement] } .where { Tags.name eq tag }
.mapNotNull { Announcement.findById(it) } .orderBy(AnnouncementTags.announcement to SortOrder.DESC)
.maxByOrNull { it.id } .limit(1)
.firstNotNullOfOrNull { Announcement.findById(it[AnnouncementTags.announcement]) }
latestAnnouncementForTag?.let { latestAnnouncementByTag[tag] = it } latestAnnouncementForTag?.let { latestAnnouncementByTag[tag] = it }
} }
@ -64,16 +65,16 @@ internal class AnnouncementRepository(private val database: Database) {
latestAnnouncement.toApiResponseAnnouncement() latestAnnouncement.toApiResponseAnnouncement()
} }
suspend fun latest(tags: Set<Int>) = transaction { suspend fun latest(tags: Set<String>) = transaction {
tags.mapNotNull { tag -> latestAnnouncementByTag[tag] }.toApiAnnouncement() tags.mapNotNull { tag -> latestAnnouncementByTag[tag] }.toApiAnnouncement()
} }
fun latestId() = latestAnnouncement?.id?.value.toApiResponseAnnouncementId() fun latestId() = latestAnnouncement?.id?.value.toApiResponseAnnouncementId()
fun latestId(tags: Set<Int>) = fun latestId(tags: Set<String>) =
tags.map { tag -> latestAnnouncementByTag[tag]?.id?.value }.toApiResponseAnnouncementId() tags.map { tag -> latestAnnouncementByTag[tag]?.id?.value }.toApiResponseAnnouncementId()
suspend fun paged(cursor: Int, count: Int, tags: Set<Int>?, archived: Boolean) = transaction { suspend fun paged(cursor: Int, count: Int, tags: Set<String>?, archived: Boolean) = transaction {
Announcement.find { Announcement.find {
fun idLessEq() = Announcements.id lessEq cursor fun idLessEq() = Announcements.id lessEq cursor
fun archivedAtIsNull() = Announcements.archivedAt.isNull() fun archivedAtIsNull() = Announcements.archivedAt.isNull()
@ -92,12 +93,12 @@ internal class AnnouncementRepository(private val database: Database) {
archivedAtIsNull() or archivedAtGreaterNow() archivedAtIsNull() or archivedAtGreaterNow()
} }
fun hasTags() = tags.mapNotNull { Tag.findById(it)?.id }.let { tags -> fun hasTags() = Announcements.id inSubQuery (
Announcements.id inSubQuery Announcements.leftJoin(AnnouncementTags) Tags.innerJoin(AnnouncementTags)
.select(AnnouncementTags.announcement) .select(AnnouncementTags.announcement)
.where { AnnouncementTags.tag inList tags } .where { Tags.name inList tags }
.withDistinct() .withDistinct()
} )
idLessEq() and archivedAtGreaterOrNullOrTrue() and hasTags() idLessEq() and archivedAtGreaterOrNullOrTrue() and hasTags()
} }
@ -165,7 +166,7 @@ internal class AnnouncementRepository(private val database: Database) {
// Delete the tag if no other announcements are referencing it. // Delete the tag if no other announcements are referencing it.
// One count means that the announcement is the only one referencing the tag. // One count means that the announcement is the only one referencing the tag.
announcement.tags.filter { tag -> tag.announcements.count() == 1L }.forEach { tag -> announcement.tags.filter { tag -> tag.announcements.count() == 1L }.forEach { tag ->
latestAnnouncementByTag -= tag.id.value latestAnnouncementByTag -= tag.name
tag.delete() tag.delete()
} }
@ -250,7 +251,7 @@ internal class AnnouncementRepository(private val database: Database) {
title, title,
content, content,
attachments.map { it.url }, attachments.map { it.url },
tags.map { it.id.value }, tags.map { it.name },
createdAt, createdAt,
archivedAt, archivedAt,
level, level,
@ -259,7 +260,7 @@ internal class AnnouncementRepository(private val database: Database) {
private fun Iterable<Announcement>.toApiAnnouncement() = map { it.toApiResponseAnnouncement()!! } private fun Iterable<Announcement>.toApiAnnouncement() = map { it.toApiResponseAnnouncement()!! }
private fun Iterable<Tag>.toApiTag() = map { ApiAnnouncementTag(it.id.value, it.name) } private fun Iterable<Tag>.toApiTag() = map { ApiAnnouncementTag(it.name) }
private fun Int?.toApiResponseAnnouncementId() = this?.let { ApiResponseAnnouncementId(this) } private fun Int?.toApiResponseAnnouncementId() = this?.let { ApiResponseAnnouncementId(this) }

View File

@ -8,7 +8,10 @@ import app.revanced.api.configuration.schema.ApiAnnouncement
import app.revanced.api.configuration.schema.ApiResponseAnnouncement import app.revanced.api.configuration.schema.ApiResponseAnnouncement
import app.revanced.api.configuration.schema.ApiResponseAnnouncementId import app.revanced.api.configuration.schema.ApiResponseAnnouncementId
import app.revanced.api.configuration.services.AnnouncementService import app.revanced.api.configuration.services.AnnouncementService
import io.bkbn.kompendium.core.metadata.* import io.bkbn.kompendium.core.metadata.DeleteInfo
import io.bkbn.kompendium.core.metadata.GetInfo
import io.bkbn.kompendium.core.metadata.PatchInfo
import io.bkbn.kompendium.core.metadata.PostInfo
import io.bkbn.kompendium.json.schema.definition.TypeDefinition import io.bkbn.kompendium.json.schema.definition.TypeDefinition
import io.bkbn.kompendium.oas.payload.Parameter import io.bkbn.kompendium.oas.payload.Parameter
import io.ktor.http.* import io.ktor.http.*
@ -35,7 +38,7 @@ internal fun Route.announcementsRoute() = route("announcements") {
val tags = call.parameters.getAll("tag") val tags = call.parameters.getAll("tag")
val archived = call.parameters["archived"]?.toBoolean() ?: true val archived = call.parameters["archived"]?.toBoolean() ?: true
call.respond(announcementService.paged(cursor, count, tags?.map { it.toInt() }?.toSet(), archived)) call.respond(announcementService.paged(cursor, count, tags?.toSet(), archived))
} }
} }
@ -55,7 +58,7 @@ internal fun Route.announcementsRoute() = route("announcements") {
val tags = call.parameters.getAll("tag") val tags = call.parameters.getAll("tag")
if (tags?.isNotEmpty() == true) { if (tags?.isNotEmpty() == true) {
call.respond(announcementService.latest(tags.map { it.toInt() }.toSet())) call.respond(announcementService.latest(tags.toSet()))
} else { } else {
call.respondOrNotFound(announcementService.latest()) call.respondOrNotFound(announcementService.latest())
} }
@ -68,7 +71,7 @@ internal fun Route.announcementsRoute() = route("announcements") {
val tags = call.parameters.getAll("tag") val tags = call.parameters.getAll("tag")
if (tags?.isNotEmpty() == true) { if (tags?.isNotEmpty() == true) {
call.respond(announcementService.latestId(tags.map { it.toInt() }.toSet())) call.respond(announcementService.latestId(tags.toSet()))
} else { } else {
call.respondOrNotFound(announcementService.latestId()) call.respondOrNotFound(announcementService.latestId())
} }
@ -146,8 +149,8 @@ private fun Route.installAnnouncementsRouteDocumentation() = installNotarizedRou
Parameter( Parameter(
name = "tag", name = "tag",
`in` = Parameter.Location.query, `in` = Parameter.Location.query,
schema = TypeDefinition.INT, schema = TypeDefinition.STRING,
description = "The tag IDs to filter the announcements by. Default is all tags", description = "The tags to filter the announcements by. Default is all tags",
required = false, required = false,
), ),
Parameter( Parameter(
@ -193,8 +196,8 @@ private fun Route.installAnnouncementsLatestRouteDocumentation() = installNotari
Parameter( Parameter(
name = "tag", name = "tag",
`in` = Parameter.Location.query, `in` = Parameter.Location.query,
schema = TypeDefinition.INT, schema = TypeDefinition.STRING,
description = "The tag IDs to filter the latest announcements by", description = "The tags to filter the latest announcements by",
required = false, required = false,
), ),
) )
@ -228,8 +231,8 @@ private fun Route.installAnnouncementsLatestIdRouteDocumentation() = installNota
Parameter( Parameter(
name = "tag", name = "tag",
`in` = Parameter.Location.query, `in` = Parameter.Location.query,
schema = TypeDefinition.INT, schema = TypeDefinition.STRING,
description = "The tag IDs to filter the latest announcements by", description = "The tags to filter the latest announcements by",
required = false, required = false,
), ),
) )

View File

@ -76,7 +76,7 @@ class ApiResponseAnnouncement(
// Using a list instead of a set because set semantics are unnecessary here. // Using a list instead of a set because set semantics are unnecessary here.
val attachments: List<String> = emptyList(), val attachments: List<String> = emptyList(),
// Using a list instead of a set because set semantics are unnecessary here. // Using a list instead of a set because set semantics are unnecessary here.
val tags: List<Int> = emptyList(), val tags: List<String> = emptyList(),
val createdAt: LocalDateTime, val createdAt: LocalDateTime,
val archivedAt: LocalDateTime? = null, val archivedAt: LocalDateTime? = null,
val level: Int = 0, val level: Int = 0,
@ -94,7 +94,6 @@ class ApiAnnouncementArchivedAt(
@Serializable @Serializable
class ApiAnnouncementTag( class ApiAnnouncementTag(
val id: Int,
val name: String, val name: String,
) )

View File

@ -6,15 +6,15 @@ import app.revanced.api.configuration.schema.ApiAnnouncement
internal class AnnouncementService( internal class AnnouncementService(
private val announcementRepository: AnnouncementRepository, private val announcementRepository: AnnouncementRepository,
) { ) {
suspend fun latest(tags: Set<Int>) = announcementRepository.latest(tags) suspend fun latest(tags: Set<String>) = announcementRepository.latest(tags)
suspend fun latest() = announcementRepository.latest() suspend fun latest() = announcementRepository.latest()
fun latestId(tags: Set<Int>) = announcementRepository.latestId(tags) fun latestId(tags: Set<String>) = announcementRepository.latestId(tags)
fun latestId() = announcementRepository.latestId() fun latestId() = announcementRepository.latestId()
suspend fun paged(cursor: Int, limit: Int, tags: Set<Int>?, archived: Boolean) = suspend fun paged(cursor: Int, limit: Int, tags: Set<String>?, archived: Boolean) =
announcementRepository.paged(cursor, limit, tags, archived) announcementRepository.paged(cursor, limit, tags, archived)
suspend fun get(id: Int) = announcementRepository.get(id) suspend fun get(id: Int) = announcementRepository.get(id)

View File

@ -5,8 +5,10 @@ import app.revanced.api.configuration.schema.ApiAnnouncement
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import kotlinx.datetime.toKotlinLocalDateTime import kotlinx.datetime.toKotlinLocalDateTime
import org.jetbrains.exposed.sql.Database import org.jetbrains.exposed.sql.Database
import org.junit.jupiter.api.*
import org.junit.jupiter.api.Assertions.assertNull import org.junit.jupiter.api.Assertions.assertNull
import org.junit.jupiter.api.BeforeAll
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import java.time.LocalDateTime import java.time.LocalDateTime
import kotlin.test.assertEquals import kotlin.test.assertEquals
import kotlin.test.assertNotNull import kotlin.test.assertNotNull
@ -84,27 +86,22 @@ private object AnnouncementServiceTest {
announcementService.new(ApiAnnouncement(title = "2", tags = listOf("tag1", "tag3"))) announcementService.new(ApiAnnouncement(title = "2", tags = listOf("tag1", "tag3")))
announcementService.new(ApiAnnouncement(title = "3", tags = listOf("tag1", "tag4"))) announcementService.new(ApiAnnouncement(title = "3", tags = listOf("tag1", "tag4")))
val tag2 = announcementService.tags().find { it.name == "tag2" }!!.id assert(announcementService.latest(setOf("tag2")).first().title == "1")
assert(announcementService.latest(setOf(tag2)).first().title == "1") assert(announcementService.latest(setOf("tag3")).last().title == "2")
val tag3 = announcementService.tags().find { it.name == "tag3" }!!.id val announcement2and3 = announcementService.latest(setOf("tag1", "tag3"))
assert(announcementService.latest(setOf(tag3)).last().title == "2")
val tag1and3 =
announcementService.tags().filter { it.name == "tag1" || it.name == "tag3" }.map { it.id }.toSet()
val announcement2and3 = announcementService.latest(tag1and3)
assert(announcement2and3.size == 2) assert(announcement2and3.size == 2)
assert(announcement2and3.any { it.title == "2" }) assert(announcement2and3.any { it.title == "2" })
assert(announcement2and3.any { it.title == "3" }) assert(announcement2and3.any { it.title == "3" })
announcementService.delete(announcementService.latestId()!!.id) announcementService.delete(announcementService.latestId()!!.id)
assert(announcementService.latest(tag1and3).first().title == "2") assert(announcementService.latest(setOf("tag1", "tag3")).first().title == "2")
announcementService.delete(announcementService.latestId()!!.id) announcementService.delete(announcementService.latestId()!!.id)
assert(announcementService.latest(tag1and3).first().title == "1") assert(announcementService.latest(setOf("tag1", "tag3")).first().title == "1")
announcementService.delete(announcementService.latestId()!!.id) announcementService.delete(announcementService.latestId()!!.id)
assert(announcementService.latest(tag1and3).isEmpty()) assert(announcementService.latest(setOf("tag1", "tag3")).isEmpty())
assert(announcementService.tags().isEmpty()) assert(announcementService.tags().isEmpty())
} }
@ -183,7 +180,7 @@ private object AnnouncementServiceTest {
val tags = announcementService.tags() val tags = announcementService.tags()
assertEquals(5, tags.size, "Returns correct number of newly created tags") assertEquals(5, tags.size, "Returns correct number of newly created tags")
val announcements3 = announcementService.paged(5, 5, setOf(tags[1].id), true) val announcements3 = announcementService.paged(5, 5, setOf(tags[1].name), true)
assertEquals(4, announcements3.size, "Filters announcements by tag") assertEquals(4, announcements3.size, "Filters announcements by tag")
val announcements4 = announcementService.paged(Int.MAX_VALUE, 10, null, false) val announcements4 = announcementService.paged(Int.MAX_VALUE, 10, null, false)