mirror of
https://github.com/revanced/revanced-api.git
synced 2025-04-29 22:24:31 +02:00
perf: Make async db transactions and use List instead of Set
This commit is contained in:
parent
01780188b9
commit
a7d1892343
@ -27,6 +27,7 @@ import kotlinx.serialization.ExperimentalSerializationApi
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonNamingStrategy
|
||||
import org.jetbrains.exposed.sql.Database
|
||||
import org.jetbrains.exposed.sql.transactions.TransactionManager
|
||||
import org.koin.core.module.dsl.singleOf
|
||||
import org.koin.core.parameter.parameterArrayOf
|
||||
import org.koin.dsl.module
|
||||
@ -41,6 +42,7 @@ fun Application.configureDependencies(
|
||||
single {
|
||||
Dotenv.configure().load()
|
||||
}
|
||||
|
||||
factory { params ->
|
||||
val defaultRequestUri: String = params.get<String>()
|
||||
val configBlock = params.getOrNull<(HttpClientConfig<OkHttpConfig>.() -> Unit)>() ?: {}
|
||||
@ -54,17 +56,6 @@ fun Application.configureDependencies(
|
||||
}
|
||||
|
||||
val repositoryModule = module {
|
||||
single {
|
||||
val dotenv = get<Dotenv>()
|
||||
|
||||
Database.connect(
|
||||
url = dotenv["DB_URL"],
|
||||
user = dotenv["DB_USER"],
|
||||
password = dotenv["DB_PASSWORD"],
|
||||
driver = "org.h2.Driver",
|
||||
)
|
||||
}
|
||||
|
||||
single<BackendRepository> {
|
||||
GitHubBackendRepository(
|
||||
get {
|
||||
@ -106,7 +97,17 @@ fun Application.configureDependencies(
|
||||
Toml.decodeFromStream(configFile.inputStream())
|
||||
}
|
||||
|
||||
singleOf(::AnnouncementRepository)
|
||||
single {
|
||||
val dotenv = get<Dotenv>()
|
||||
|
||||
TransactionManager.defaultDatabase = Database.connect(
|
||||
url = dotenv["DB_URL"],
|
||||
user = dotenv["DB_USER"],
|
||||
password = dotenv["DB_PASSWORD"],
|
||||
)
|
||||
|
||||
AnnouncementRepository()
|
||||
}
|
||||
}
|
||||
|
||||
val serviceModule = module {
|
||||
|
@ -1,47 +1,41 @@
|
||||
package app.revanced.api.configuration.repository
|
||||
|
||||
import app.revanced.api.configuration.repository.AnnouncementRepository.AttachmentTable.announcement
|
||||
import app.revanced.api.configuration.schema.APIAnnouncement
|
||||
import app.revanced.api.configuration.schema.APIResponseAnnouncement
|
||||
import app.revanced.api.configuration.schema.APIResponseAnnouncementId
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.datetime.*
|
||||
import org.jetbrains.exposed.dao.IntEntity
|
||||
import org.jetbrains.exposed.dao.IntEntityClass
|
||||
import org.jetbrains.exposed.dao.id.EntityID
|
||||
import org.jetbrains.exposed.dao.id.IntIdTable
|
||||
import org.jetbrains.exposed.dao.load
|
||||
import org.jetbrains.exposed.sql.*
|
||||
import org.jetbrains.exposed.sql.kotlin.datetime.CurrentDateTime
|
||||
import org.jetbrains.exposed.sql.kotlin.datetime.datetime
|
||||
import org.jetbrains.exposed.sql.transactions.experimental.newSuspendedTransaction
|
||||
import org.jetbrains.exposed.sql.transactions.experimental.suspendedTransactionAsync
|
||||
|
||||
internal class AnnouncementRepository(private val database: Database) {
|
||||
internal class AnnouncementRepository {
|
||||
init {
|
||||
runBlocking {
|
||||
transaction {
|
||||
SchemaUtils.create(AnnouncementTable, AttachmentTable)
|
||||
SchemaUtils.create(Announcements, Attachments)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun all() = transaction {
|
||||
buildSet {
|
||||
AnnouncementEntity.all().forEach { announcement ->
|
||||
add(announcement.toApi())
|
||||
}
|
||||
}
|
||||
Announcement.all()
|
||||
}
|
||||
|
||||
suspend fun all(channel: String) = transaction {
|
||||
buildSet {
|
||||
AnnouncementEntity.find { AnnouncementTable.channel eq channel }.forEach { announcement ->
|
||||
add(announcement.toApi())
|
||||
}
|
||||
}
|
||||
Announcement.find { Announcements.channel eq channel }
|
||||
}
|
||||
|
||||
suspend fun delete(id: Int) = transaction {
|
||||
val announcement = AnnouncementEntity.findById(id) ?: return@transaction
|
||||
val announcement = Announcement.findById(id) ?: return@transaction
|
||||
|
||||
announcement.delete()
|
||||
}
|
||||
@ -49,21 +43,21 @@ internal class AnnouncementRepository(private val database: Database) {
|
||||
// TODO: These are inefficient, but I'm not sure how to make them more efficient.
|
||||
|
||||
suspend fun latest() = transaction {
|
||||
AnnouncementEntity.all().maxByOrNull { it.createdAt }?.toApi()
|
||||
Announcement.all().maxByOrNull { it.id }?.load(Announcement::attachments)
|
||||
}
|
||||
|
||||
suspend fun latest(channel: String) = transaction {
|
||||
AnnouncementEntity.find { AnnouncementTable.channel eq channel }.maxByOrNull { it.createdAt }?.toApi()
|
||||
Announcement.find { Announcements.channel eq channel }.maxByOrNull { it.id }?.load(Announcement::attachments)
|
||||
}
|
||||
|
||||
suspend fun latestId() = transaction {
|
||||
AnnouncementEntity.all().maxByOrNull { it.createdAt }?.id?.value?.let {
|
||||
Announcement.all().maxByOrNull { it.id }?.id?.value?.let {
|
||||
APIResponseAnnouncementId(it)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun latestId(channel: String) = transaction {
|
||||
AnnouncementEntity.find { AnnouncementTable.channel eq channel }.maxByOrNull { it.createdAt }?.id?.value?.let {
|
||||
Announcement.find { Announcements.channel eq channel }.maxByOrNull { it.id }?.id?.value?.let {
|
||||
APIResponseAnnouncementId(it)
|
||||
}
|
||||
}
|
||||
@ -72,106 +66,98 @@ internal class AnnouncementRepository(private val database: Database) {
|
||||
id: Int,
|
||||
archivedAt: LocalDateTime?,
|
||||
) = transaction {
|
||||
AnnouncementEntity.findById(id)?.apply {
|
||||
this.archivedAt = archivedAt ?: java.time.LocalDateTime.now().toKotlinLocalDateTime()
|
||||
Announcement.findByIdAndUpdate(id) {
|
||||
it.archivedAt = archivedAt ?: java.time.LocalDateTime.now().toKotlinLocalDateTime()
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun unarchive(id: Int) = transaction {
|
||||
AnnouncementEntity.findById(id)?.apply {
|
||||
archivedAt = null
|
||||
Announcement.findByIdAndUpdate(id) {
|
||||
it.archivedAt = null
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun new(new: APIAnnouncement) = transaction {
|
||||
AnnouncementEntity.new announcement@{
|
||||
Announcement.new {
|
||||
author = new.author
|
||||
title = new.title
|
||||
content = new.content
|
||||
channel = new.channel
|
||||
createdAt = Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault())
|
||||
archivedAt = new.archivedAt
|
||||
level = new.level
|
||||
}.also { newAnnouncement ->
|
||||
new.attachmentUrls.map {
|
||||
AttachmentEntity.new {
|
||||
url = it
|
||||
announcement = newAnnouncement
|
||||
new.attachmentUrls.map { newUrl ->
|
||||
suspendedTransactionAsync {
|
||||
Attachment.new {
|
||||
url = newUrl
|
||||
announcement = newAnnouncement
|
||||
}
|
||||
}
|
||||
}
|
||||
}.awaitAll()
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun update(id: Int, new: APIAnnouncement) = transaction {
|
||||
AnnouncementEntity.findById(id)?.apply {
|
||||
author = new.author
|
||||
title = new.title
|
||||
content = new.content
|
||||
channel = new.channel
|
||||
archivedAt = new.archivedAt
|
||||
level = new.level
|
||||
|
||||
attachments.forEach(AttachmentEntity::delete)
|
||||
new.attachmentUrls.map {
|
||||
AttachmentEntity.new {
|
||||
url = it
|
||||
announcement = this@apply
|
||||
Announcement.findByIdAndUpdate(id) {
|
||||
it.author = new.author
|
||||
it.title = new.title
|
||||
it.content = new.content
|
||||
it.channel = new.channel
|
||||
it.archivedAt = new.archivedAt
|
||||
it.level = new.level
|
||||
}?.also { newAnnouncement ->
|
||||
newAnnouncement.attachments.map {
|
||||
suspendedTransactionAsync {
|
||||
it.delete()
|
||||
}
|
||||
}
|
||||
}.awaitAll()
|
||||
|
||||
new.attachmentUrls.map { newUrl ->
|
||||
suspendedTransactionAsync {
|
||||
Attachment.new {
|
||||
url = newUrl
|
||||
announcement = newAnnouncement
|
||||
}
|
||||
}
|
||||
}.awaitAll()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun <T> transaction(statement: Transaction.() -> T) =
|
||||
newSuspendedTransaction(Dispatchers.IO, database, statement = statement)
|
||||
private suspend fun <T> transaction(statement: suspend Transaction.() -> T) =
|
||||
newSuspendedTransaction(Dispatchers.IO, statement = statement)
|
||||
|
||||
private object AnnouncementTable : IntIdTable() {
|
||||
private object Announcements : IntIdTable() {
|
||||
val author = varchar("author", 32).nullable()
|
||||
val title = varchar("title", 64)
|
||||
val content = text("content").nullable()
|
||||
val channel = varchar("channel", 16).nullable()
|
||||
val createdAt = datetime("createdAt")
|
||||
val createdAt = datetime("createdAt").defaultExpression(CurrentDateTime)
|
||||
val archivedAt = datetime("archivedAt").nullable()
|
||||
val level = integer("level")
|
||||
}
|
||||
|
||||
private object AttachmentTable : IntIdTable() {
|
||||
private object Attachments : IntIdTable() {
|
||||
val url = varchar("url", 256)
|
||||
val announcement = reference("announcement", AnnouncementTable, onDelete = ReferenceOption.CASCADE)
|
||||
val announcement = reference("announcement", Announcements, onDelete = ReferenceOption.CASCADE)
|
||||
}
|
||||
|
||||
class AnnouncementEntity(id: EntityID<Int>) : IntEntity(id) {
|
||||
companion object : IntEntityClass<AnnouncementEntity>(AnnouncementTable)
|
||||
class Announcement(id: EntityID<Int>) : IntEntity(id) {
|
||||
companion object : IntEntityClass<Announcement>(Announcements)
|
||||
|
||||
var author by AnnouncementTable.author
|
||||
var title by AnnouncementTable.title
|
||||
var content by AnnouncementTable.content
|
||||
val attachments by AttachmentEntity referrersOn announcement
|
||||
var channel by AnnouncementTable.channel
|
||||
var createdAt by AnnouncementTable.createdAt
|
||||
var archivedAt by AnnouncementTable.archivedAt
|
||||
var level by AnnouncementTable.level
|
||||
|
||||
fun toApi() = APIResponseAnnouncement(
|
||||
id.value,
|
||||
author,
|
||||
title,
|
||||
content,
|
||||
attachmentUrls = buildSet {
|
||||
attachments.forEach {
|
||||
add(it.url)
|
||||
}
|
||||
},
|
||||
channel,
|
||||
createdAt,
|
||||
archivedAt,
|
||||
level,
|
||||
)
|
||||
var author by Announcements.author
|
||||
var title by Announcements.title
|
||||
var content by Announcements.content
|
||||
val attachments by Attachment referrersOn Attachments.announcement
|
||||
var channel by Announcements.channel
|
||||
var createdAt by Announcements.createdAt
|
||||
var archivedAt by Announcements.archivedAt
|
||||
var level by Announcements.level
|
||||
}
|
||||
|
||||
class AttachmentEntity(id: EntityID<Int>) : IntEntity(id) {
|
||||
companion object : IntEntityClass<AttachmentEntity>(AttachmentTable)
|
||||
class Attachment(id: EntityID<Int>) : IntEntity(id) {
|
||||
companion object : IntEntityClass<Attachment>(Attachments)
|
||||
|
||||
var url by AttachmentTable.url
|
||||
var announcement by AnnouncementEntity referencedOn AttachmentTable.announcement
|
||||
var url by Attachments.url
|
||||
var announcement by Announcement referencedOn Attachments.announcement
|
||||
}
|
||||
}
|
||||
|
@ -30,7 +30,8 @@ abstract class BackendRepository internal constructor(
|
||||
* @property members The members of the organization.
|
||||
*/
|
||||
class BackendOrganization(
|
||||
val members: Set<BackendMember>,
|
||||
// Using a list instead of a set because set semantics are unnecessary here.
|
||||
val members: List<BackendMember>,
|
||||
) {
|
||||
/**
|
||||
* A member of an organization.
|
||||
@ -55,7 +56,8 @@ abstract class BackendRepository internal constructor(
|
||||
* @property url The URL to the GPG master key.
|
||||
*/
|
||||
class GpgKeys(
|
||||
val ids: Set<String>,
|
||||
// Using a list instead of a set because set semantics are unnecessary here.
|
||||
val ids: List<String>,
|
||||
val url: String,
|
||||
)
|
||||
}
|
||||
@ -66,7 +68,8 @@ abstract class BackendRepository internal constructor(
|
||||
* @property contributors The contributors of the repository.
|
||||
*/
|
||||
class BackendRepository(
|
||||
val contributors: Set<BackendContributor>,
|
||||
// Using a list instead of a set because set semantics are unnecessary here.
|
||||
val contributors: List<BackendContributor>,
|
||||
) {
|
||||
/**
|
||||
* A contributor of a repository.
|
||||
@ -95,10 +98,11 @@ abstract class BackendRepository internal constructor(
|
||||
val tag: String,
|
||||
val releaseNote: String,
|
||||
val createdAt: LocalDateTime,
|
||||
val assets: Set<BackendAsset>,
|
||||
// Using a list instead of a set because set semantics are unnecessary here.
|
||||
val assets: List<BackendAsset>,
|
||||
) {
|
||||
companion object {
|
||||
fun Set<BackendAsset>.first(assetRegex: Regex) = first { assetRegex.containsMatchIn(it.name) }
|
||||
fun List<BackendAsset>.first(assetRegex: Regex) = first { assetRegex.containsMatchIn(it.name) }
|
||||
}
|
||||
|
||||
/**
|
||||
@ -149,7 +153,7 @@ abstract class BackendRepository internal constructor(
|
||||
* @param repository The name of the repository.
|
||||
* @return The contributors.
|
||||
*/
|
||||
abstract suspend fun contributors(owner: String, repository: String): Set<BackendOrganization.BackendRepository.BackendContributor>
|
||||
abstract suspend fun contributors(owner: String, repository: String): List<BackendOrganization.BackendRepository.BackendContributor>
|
||||
|
||||
/**
|
||||
* Get the members of an organization.
|
||||
@ -157,7 +161,7 @@ abstract class BackendRepository internal constructor(
|
||||
* @param organization The name of the organization.
|
||||
* @return The members.
|
||||
*/
|
||||
abstract suspend fun members(organization: String): Set<BackendOrganization.BackendMember>
|
||||
abstract suspend fun members(organization: String): List<BackendOrganization.BackendMember>
|
||||
|
||||
/**
|
||||
* Get the rate limit of the backend.
|
||||
|
@ -39,15 +39,15 @@ class GitHubBackendRepository(client: HttpClient) : BackendRepository(client) {
|
||||
name = it.name,
|
||||
downloadUrl = it.browserDownloadUrl,
|
||||
)
|
||||
}.toSet(),
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun contributors(
|
||||
owner: String,
|
||||
repository: String,
|
||||
): Set<BackendContributor> {
|
||||
val contributors: Set<GitHubContributor> = client.get(
|
||||
): List<BackendContributor> {
|
||||
val contributors: List<GitHubContributor> = client.get(
|
||||
Contributors(
|
||||
owner,
|
||||
repository,
|
||||
@ -61,12 +61,12 @@ class GitHubBackendRepository(client: HttpClient) : BackendRepository(client) {
|
||||
url = it.htmlUrl,
|
||||
contributions = it.contributions,
|
||||
)
|
||||
}.toSet()
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun members(organization: String): Set<BackendMember> {
|
||||
override suspend fun members(organization: String): List<BackendMember> {
|
||||
// Get the list of members of the organization.
|
||||
val members: Set<GitHubOrganization.GitHubMember> = client.get(Organization.Members(organization)).body()
|
||||
val members: List<GitHubOrganization.GitHubMember> = client.get(Organization.Members(organization)).body()
|
||||
|
||||
return coroutineScope {
|
||||
members.map { member ->
|
||||
@ -78,7 +78,7 @@ class GitHubBackendRepository(client: HttpClient) : BackendRepository(client) {
|
||||
},
|
||||
async {
|
||||
// Get the GPG key of the user.
|
||||
client.get(User.GpgKeys(member.login)).body<Set<GitHubUser.GitHubGpgKey>>()
|
||||
client.get(User.GpgKeys(member.login)).body<List<GitHubUser.GitHubGpgKey>>()
|
||||
},
|
||||
)
|
||||
}
|
||||
@ -87,7 +87,7 @@ class GitHubBackendRepository(client: HttpClient) : BackendRepository(client) {
|
||||
val user = responses[0] as GitHubUser
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val gpgKeys = responses[1] as Set<GitHubUser.GitHubGpgKey>
|
||||
val gpgKeys = responses[1] as List<GitHubUser.GitHubGpgKey>
|
||||
|
||||
BackendMember(
|
||||
name = user.login,
|
||||
@ -96,11 +96,11 @@ class GitHubBackendRepository(client: HttpClient) : BackendRepository(client) {
|
||||
bio = user.bio,
|
||||
gpgKeys =
|
||||
BackendMember.GpgKeys(
|
||||
ids = gpgKeys.map { it.keyId }.toSet(),
|
||||
ids = gpgKeys.map { it.keyId },
|
||||
url = "https://api.github.com/users/${user.login}.gpg",
|
||||
),
|
||||
)
|
||||
}.toSet()
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun rateLimit(): BackendRateLimit {
|
||||
@ -153,7 +153,8 @@ class GitHubOrganization {
|
||||
@Serializable
|
||||
class GitHubRelease(
|
||||
val tagName: String,
|
||||
val assets: Set<GitHubAsset>,
|
||||
// Using a list instead of a set because set semantics are unnecessary here.
|
||||
val assets: List<GitHubAsset>,
|
||||
val createdAt: Instant,
|
||||
val body: String,
|
||||
) {
|
||||
|
@ -8,7 +8,8 @@ class APIRelease(
|
||||
val version: String,
|
||||
val createdAt: LocalDateTime,
|
||||
val description: String,
|
||||
val assets: Set<APIAsset>,
|
||||
// Using a list instead of a set because set semantics are unnecessary here.
|
||||
val assets: List<APIAsset>,
|
||||
)
|
||||
|
||||
interface APIUser {
|
||||
@ -42,7 +43,8 @@ class APIContributor(
|
||||
@Serializable
|
||||
class APIContributable(
|
||||
val name: String,
|
||||
val contributors: Set<APIContributor>,
|
||||
// Using a list instead of a set because set semantics are unnecessary here.
|
||||
val contributors: List<APIContributor>,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
@ -69,7 +71,8 @@ class APIAnnouncement(
|
||||
val author: String? = null,
|
||||
val title: String,
|
||||
val content: String? = null,
|
||||
val attachmentUrls: Set<String> = emptySet(),
|
||||
// Using a list instead of a set because set semantics are unnecessary here.
|
||||
val attachmentUrls: List<String> = emptyList(),
|
||||
val channel: String? = null,
|
||||
val archivedAt: LocalDateTime? = null,
|
||||
val level: Int = 0,
|
||||
@ -81,7 +84,8 @@ class APIResponseAnnouncement(
|
||||
val author: String? = null,
|
||||
val title: String,
|
||||
val content: String? = null,
|
||||
val attachmentUrls: Set<String> = emptySet(),
|
||||
// Using a list instead of a set because set semantics are unnecessary here.
|
||||
val attachmentUrls: List<String> = emptyList(),
|
||||
val channel: String? = null,
|
||||
val createdAt: LocalDateTime,
|
||||
val archivedAt: LocalDateTime? = null,
|
||||
|
@ -2,6 +2,7 @@ package app.revanced.api.configuration.services
|
||||
|
||||
import app.revanced.api.configuration.repository.AnnouncementRepository
|
||||
import app.revanced.api.configuration.schema.APIAnnouncement
|
||||
import app.revanced.api.configuration.schema.APIResponseAnnouncement
|
||||
import app.revanced.api.configuration.schema.APIResponseAnnouncementId
|
||||
import kotlinx.datetime.LocalDateTime
|
||||
|
||||
@ -11,11 +12,11 @@ internal class AnnouncementService(
|
||||
suspend fun latestId(channel: String): APIResponseAnnouncementId? = announcementRepository.latestId(channel)
|
||||
suspend fun latestId(): APIResponseAnnouncementId? = announcementRepository.latestId()
|
||||
|
||||
suspend fun latest(channel: String) = announcementRepository.latest(channel)
|
||||
suspend fun latest() = announcementRepository.latest()
|
||||
suspend fun latest(channel: String) = announcementRepository.latest(channel)?.toApi()
|
||||
suspend fun latest() = announcementRepository.latest()?.toApi()
|
||||
|
||||
suspend fun all(channel: String) = announcementRepository.all(channel)
|
||||
suspend fun all() = announcementRepository.all()
|
||||
suspend fun all(channel: String) = announcementRepository.all(channel).map { it.toApi() }
|
||||
suspend fun all() = announcementRepository.all().map { it.toApi() }
|
||||
|
||||
suspend fun new(new: APIAnnouncement) {
|
||||
announcementRepository.new(new)
|
||||
@ -32,4 +33,16 @@ internal class AnnouncementService(
|
||||
suspend fun delete(id: Int) {
|
||||
announcementRepository.delete(id)
|
||||
}
|
||||
|
||||
private fun AnnouncementRepository.Announcement.toApi() = APIResponseAnnouncement(
|
||||
id.value,
|
||||
author,
|
||||
title,
|
||||
content,
|
||||
attachments.map { it.url },
|
||||
channel,
|
||||
createdAt,
|
||||
archivedAt,
|
||||
level,
|
||||
)
|
||||
}
|
||||
|
@ -19,11 +19,11 @@ internal class ApiService(
|
||||
it,
|
||||
backendRepository.contributors(configurationRepository.organization, it).map {
|
||||
APIContributor(it.name, it.avatarUrl, it.url, it.contributions)
|
||||
}.toSet(),
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}.awaitAll().toSet()
|
||||
}.awaitAll()
|
||||
|
||||
suspend fun team() = backendRepository.members(configurationRepository.organization).map { member ->
|
||||
APIMember(
|
||||
@ -41,7 +41,7 @@ internal class ApiService(
|
||||
},
|
||||
|
||||
)
|
||||
}.toSet()
|
||||
}
|
||||
|
||||
suspend fun rateLimit() = backendRepository.rateLimit()?.let {
|
||||
APIRateLimit(it.limit, it.remaining, it.reset)
|
||||
|
@ -51,7 +51,7 @@ internal class PatchesService(
|
||||
patchesRelease.tag,
|
||||
patchesRelease.createdAt,
|
||||
patchesRelease.releaseNote,
|
||||
setOf(patchesAsset, integrationsAsset),
|
||||
listOf(patchesAsset, integrationsAsset),
|
||||
)
|
||||
}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user