diff --git a/src/main/kotlin/app/revanced/api/configuration/repository/AnnouncementRepository.kt b/src/main/kotlin/app/revanced/api/configuration/repository/AnnouncementRepository.kt index d7ce97b..7ff88fe 100644 --- a/src/main/kotlin/app/revanced/api/configuration/repository/AnnouncementRepository.kt +++ b/src/main/kotlin/app/revanced/api/configuration/repository/AnnouncementRepository.kt @@ -6,7 +6,7 @@ import app.revanced.api.configuration.schema.ApiResponseAnnouncement import app.revanced.api.configuration.schema.ApiResponseAnnouncementId import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.runBlocking -import kotlinx.datetime.* +import kotlinx.datetime.toKotlinLocalDateTime import org.jetbrains.exposed.dao.IntEntity import org.jetbrains.exposed.dao.IntEntityClass import org.jetbrains.exposed.dao.id.EntityID @@ -20,7 +20,7 @@ import java.time.LocalDateTime internal class AnnouncementRepository(private val database: Database) { // This is better than doing a maxByOrNull { it.id } on every request. private var latestAnnouncement: Announcement? = null - private val latestAnnouncementByTag = mutableMapOf() + private val latestAnnouncementByTag = mutableMapOf() init { runBlocking { @@ -40,22 +40,23 @@ internal class AnnouncementRepository(private val database: Database) { private fun initializeLatestAnnouncements() { 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) { if (latestAnnouncement == null || latestAnnouncement!!.id.value <= new.id.value) { latestAnnouncement = new - new.tags.forEach { tag -> latestAnnouncementByTag[tag.id.value] = new } + new.tags.forEach { tag -> latestAnnouncementByTag[tag.name] = new } } } - private fun updateLatestAnnouncementForTag(tag: Int) { - val latestAnnouncementForTag = AnnouncementTags.select(AnnouncementTags.announcement) - .where { AnnouncementTags.tag eq tag } - .map { it[AnnouncementTags.announcement] } - .mapNotNull { Announcement.findById(it) } - .maxByOrNull { it.id } + private fun updateLatestAnnouncementForTag(tag: String) { + val latestAnnouncementForTag = Tags.innerJoin(AnnouncementTags) + .select(AnnouncementTags.announcement) + .where { Tags.name eq tag } + .orderBy(AnnouncementTags.announcement to SortOrder.DESC) + .limit(1) + .firstNotNullOfOrNull { Announcement.findById(it[AnnouncementTags.announcement]) } latestAnnouncementForTag?.let { latestAnnouncementByTag[tag] = it } } @@ -64,16 +65,16 @@ internal class AnnouncementRepository(private val database: Database) { latestAnnouncement.toApiResponseAnnouncement() } - suspend fun latest(tags: Set) = transaction { + suspend fun latest(tags: Set) = transaction { tags.mapNotNull { tag -> latestAnnouncementByTag[tag] }.toApiAnnouncement() } fun latestId() = latestAnnouncement?.id?.value.toApiResponseAnnouncementId() - fun latestId(tags: Set) = + fun latestId(tags: Set) = tags.map { tag -> latestAnnouncementByTag[tag]?.id?.value }.toApiResponseAnnouncementId() - suspend fun paged(cursor: Int, count: Int, tags: Set?, archived: Boolean) = transaction { + suspend fun paged(cursor: Int, count: Int, tags: Set?, archived: Boolean) = transaction { Announcement.find { fun idLessEq() = Announcements.id lessEq cursor fun archivedAtIsNull() = Announcements.archivedAt.isNull() @@ -92,12 +93,12 @@ internal class AnnouncementRepository(private val database: Database) { archivedAtIsNull() or archivedAtGreaterNow() } - fun hasTags() = tags.mapNotNull { Tag.findById(it)?.id }.let { tags -> - Announcements.id inSubQuery Announcements.leftJoin(AnnouncementTags) + fun hasTags() = Announcements.id inSubQuery ( + Tags.innerJoin(AnnouncementTags) .select(AnnouncementTags.announcement) - .where { AnnouncementTags.tag inList tags } + .where { Tags.name inList tags } .withDistinct() - } + ) 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. // One count means that the announcement is the only one referencing the tag. announcement.tags.filter { tag -> tag.announcements.count() == 1L }.forEach { tag -> - latestAnnouncementByTag -= tag.id.value + latestAnnouncementByTag -= tag.name tag.delete() } @@ -250,7 +251,7 @@ internal class AnnouncementRepository(private val database: Database) { title, content, attachments.map { it.url }, - tags.map { it.id.value }, + tags.map { it.name }, createdAt, archivedAt, level, @@ -259,7 +260,7 @@ internal class AnnouncementRepository(private val database: Database) { private fun Iterable.toApiAnnouncement() = map { it.toApiResponseAnnouncement()!! } - private fun Iterable.toApiTag() = map { ApiAnnouncementTag(it.id.value, it.name) } + private fun Iterable.toApiTag() = map { ApiAnnouncementTag(it.name) } private fun Int?.toApiResponseAnnouncementId() = this?.let { ApiResponseAnnouncementId(this) } diff --git a/src/main/kotlin/app/revanced/api/configuration/routes/Announcements.kt b/src/main/kotlin/app/revanced/api/configuration/routes/Announcements.kt index aeb1c0f..dd67e0f 100644 --- a/src/main/kotlin/app/revanced/api/configuration/routes/Announcements.kt +++ b/src/main/kotlin/app/revanced/api/configuration/routes/Announcements.kt @@ -8,7 +8,10 @@ import app.revanced.api.configuration.schema.ApiAnnouncement import app.revanced.api.configuration.schema.ApiResponseAnnouncement import app.revanced.api.configuration.schema.ApiResponseAnnouncementId 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.oas.payload.Parameter import io.ktor.http.* @@ -35,7 +38,7 @@ internal fun Route.announcementsRoute() = route("announcements") { val tags = call.parameters.getAll("tag") 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") if (tags?.isNotEmpty() == true) { - call.respond(announcementService.latest(tags.map { it.toInt() }.toSet())) + call.respond(announcementService.latest(tags.toSet())) } else { call.respondOrNotFound(announcementService.latest()) } @@ -68,7 +71,7 @@ internal fun Route.announcementsRoute() = route("announcements") { val tags = call.parameters.getAll("tag") if (tags?.isNotEmpty() == true) { - call.respond(announcementService.latestId(tags.map { it.toInt() }.toSet())) + call.respond(announcementService.latestId(tags.toSet())) } else { call.respondOrNotFound(announcementService.latestId()) } @@ -146,8 +149,8 @@ private fun Route.installAnnouncementsRouteDocumentation() = installNotarizedRou Parameter( name = "tag", `in` = Parameter.Location.query, - schema = TypeDefinition.INT, - description = "The tag IDs to filter the announcements by. Default is all tags", + schema = TypeDefinition.STRING, + description = "The tags to filter the announcements by. Default is all tags", required = false, ), Parameter( @@ -193,8 +196,8 @@ private fun Route.installAnnouncementsLatestRouteDocumentation() = installNotari Parameter( name = "tag", `in` = Parameter.Location.query, - schema = TypeDefinition.INT, - description = "The tag IDs to filter the latest announcements by", + schema = TypeDefinition.STRING, + description = "The tags to filter the latest announcements by", required = false, ), ) @@ -228,8 +231,8 @@ private fun Route.installAnnouncementsLatestIdRouteDocumentation() = installNota Parameter( name = "tag", `in` = Parameter.Location.query, - schema = TypeDefinition.INT, - description = "The tag IDs to filter the latest announcements by", + schema = TypeDefinition.STRING, + description = "The tags to filter the latest announcements by", required = false, ), ) diff --git a/src/main/kotlin/app/revanced/api/configuration/schema/APISchema.kt b/src/main/kotlin/app/revanced/api/configuration/schema/APISchema.kt index 369dc16..2d3200d 100644 --- a/src/main/kotlin/app/revanced/api/configuration/schema/APISchema.kt +++ b/src/main/kotlin/app/revanced/api/configuration/schema/APISchema.kt @@ -76,7 +76,7 @@ class ApiResponseAnnouncement( // Using a list instead of a set because set semantics are unnecessary here. val attachments: List = emptyList(), // Using a list instead of a set because set semantics are unnecessary here. - val tags: List = emptyList(), + val tags: List = emptyList(), val createdAt: LocalDateTime, val archivedAt: LocalDateTime? = null, val level: Int = 0, @@ -94,7 +94,6 @@ class ApiAnnouncementArchivedAt( @Serializable class ApiAnnouncementTag( - val id: Int, val name: String, ) diff --git a/src/main/kotlin/app/revanced/api/configuration/services/AnnouncementService.kt b/src/main/kotlin/app/revanced/api/configuration/services/AnnouncementService.kt index 434f0a5..7ba5970 100644 --- a/src/main/kotlin/app/revanced/api/configuration/services/AnnouncementService.kt +++ b/src/main/kotlin/app/revanced/api/configuration/services/AnnouncementService.kt @@ -6,15 +6,15 @@ import app.revanced.api.configuration.schema.ApiAnnouncement internal class AnnouncementService( private val announcementRepository: AnnouncementRepository, ) { - suspend fun latest(tags: Set) = announcementRepository.latest(tags) + suspend fun latest(tags: Set) = announcementRepository.latest(tags) suspend fun latest() = announcementRepository.latest() - fun latestId(tags: Set) = announcementRepository.latestId(tags) + fun latestId(tags: Set) = announcementRepository.latestId(tags) fun latestId() = announcementRepository.latestId() - suspend fun paged(cursor: Int, limit: Int, tags: Set?, archived: Boolean) = + suspend fun paged(cursor: Int, limit: Int, tags: Set?, archived: Boolean) = announcementRepository.paged(cursor, limit, tags, archived) suspend fun get(id: Int) = announcementRepository.get(id) diff --git a/src/test/kotlin/app/revanced/api/configuration/services/AnnouncementServiceTest.kt b/src/test/kotlin/app/revanced/api/configuration/services/AnnouncementServiceTest.kt index 3fdf4d6..d986b67 100644 --- a/src/test/kotlin/app/revanced/api/configuration/services/AnnouncementServiceTest.kt +++ b/src/test/kotlin/app/revanced/api/configuration/services/AnnouncementServiceTest.kt @@ -5,8 +5,10 @@ import app.revanced.api.configuration.schema.ApiAnnouncement import kotlinx.coroutines.runBlocking import kotlinx.datetime.toKotlinLocalDateTime import org.jetbrains.exposed.sql.Database -import org.junit.jupiter.api.* 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 kotlin.test.assertEquals import kotlin.test.assertNotNull @@ -84,27 +86,22 @@ private object AnnouncementServiceTest { announcementService.new(ApiAnnouncement(title = "2", tags = listOf("tag1", "tag3"))) 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 - 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) + val announcement2and3 = announcementService.latest(setOf("tag1", "tag3")) assert(announcement2and3.size == 2) assert(announcement2and3.any { it.title == "2" }) assert(announcement2and3.any { it.title == "3" }) 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) - assert(announcementService.latest(tag1and3).first().title == "1") + assert(announcementService.latest(setOf("tag1", "tag3")).first().title == "1") announcementService.delete(announcementService.latestId()!!.id) - assert(announcementService.latest(tag1and3).isEmpty()) + assert(announcementService.latest(setOf("tag1", "tag3")).isEmpty()) assert(announcementService.tags().isEmpty()) } @@ -183,7 +180,7 @@ private object AnnouncementServiceTest { val tags = announcementService.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") val announcements4 = announcementService.paged(Int.MAX_VALUE, 10, null, false)