0.4.0
This commit is contained in:
@@ -2,20 +2,30 @@ package com.insanusmokrassar.postssystem.core.exposed
|
||||
|
||||
import com.insanusmokrassar.postssystem.core.content.*
|
||||
import com.insanusmokrassar.postssystem.core.content.api.ContentAPI
|
||||
import com.insanusmokrassar.postssystem.core.exposed.content.*
|
||||
import com.insanusmokrassar.postssystem.core.utils.generateContentId
|
||||
import com.insanusmokrassar.postssystem.core.utils.pagination.*
|
||||
import kotlinx.coroutines.channels.BroadcastChannel
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.asFlow
|
||||
import kotlinx.serialization.json.Json
|
||||
import org.jetbrains.exposed.sql.*
|
||||
import org.jetbrains.exposed.sql.transactions.transaction
|
||||
|
||||
private val Content.type
|
||||
get() = when (this) {
|
||||
is TextContent -> "text"
|
||||
is BinaryContent -> "binary"
|
||||
is SpecialContent -> "special"
|
||||
}
|
||||
|
||||
private class ContentAPIDatabaseTable(
|
||||
private val database: Database
|
||||
) : Table("ContentAPI"), ContentAPI {
|
||||
private val database: Database,
|
||||
private val textHolder: ContentHolderRepo<TextContent>,
|
||||
private val binaryHolder: ContentHolderRepo<BinaryContent>,
|
||||
private val specialHolder: ContentHolderRepo<SpecialContent>
|
||||
) : Table("ContentAPI"), ContentAPI, ContentHolderRepo<Content> {
|
||||
internal val idColumn = text("_id")
|
||||
internal val dataColumn = text("data")
|
||||
internal val typeColumn = text("type")
|
||||
|
||||
init {
|
||||
transaction(database) {
|
||||
@@ -28,19 +38,48 @@ private class ContentAPIDatabaseTable(
|
||||
override val contentCreatedFlow: Flow<RegisteredContent> = contentCreatedBroadcastChannel.asFlow()
|
||||
override val contentDeletedFlow: Flow<RegisteredContent> = contentDeletedBroadcastChannel.asFlow()
|
||||
|
||||
override suspend fun createContent(content: Content): RegisteredContent? {
|
||||
private val String.holder
|
||||
get() = when (this) {
|
||||
"text" -> textHolder
|
||||
"binary" -> binaryHolder
|
||||
"special" -> specialHolder
|
||||
else -> null
|
||||
}
|
||||
|
||||
override suspend fun putContent(id: ContentId, content: Content) {
|
||||
when (content) {
|
||||
is TextContent -> textHolder.putContent(id, content)
|
||||
is BinaryContent -> binaryHolder.putContent(id, content)
|
||||
is SpecialContent -> specialHolder.putContent(id, content)
|
||||
}
|
||||
}
|
||||
override suspend fun getContent(id: ContentId): Content? = transaction(database) {
|
||||
select { idColumn.eq(id) }.limit(1).firstOrNull() ?.get(typeColumn)
|
||||
} ?.holder ?.getContent(id)
|
||||
override suspend fun removeContent(id: ContentId) {
|
||||
transaction(database) {
|
||||
select { idColumn.eq(id) }.limit(1).firstOrNull() ?.get(typeColumn)
|
||||
} ?.holder ?.removeContent(id)
|
||||
}
|
||||
|
||||
override suspend fun registerContent(content: Content): RegisteredContent? {
|
||||
val id = generateContentId()
|
||||
val type = content.type
|
||||
return transaction(database) {
|
||||
insert {
|
||||
it[idColumn] = generateContentId()
|
||||
it[dataColumn] = Json.stringify(Content.serializer(), content)
|
||||
}.getOrNull(idColumn) ?.let { id ->
|
||||
RegisteredContent(
|
||||
id,
|
||||
content
|
||||
)
|
||||
}
|
||||
it[idColumn] = id
|
||||
it[typeColumn] = type
|
||||
}.getOrNull(idColumn)
|
||||
} ?.let { id ->
|
||||
putContent(id, content)
|
||||
RegisteredContent(
|
||||
id,
|
||||
content
|
||||
)
|
||||
} ?.also {
|
||||
contentCreatedBroadcastChannel.send(it)
|
||||
} ?: null.also {
|
||||
removeContent(id)
|
||||
}
|
||||
}
|
||||
override suspend fun deleteContent(id: ContentId): Boolean {
|
||||
@@ -51,15 +90,18 @@ private class ContentAPIDatabaseTable(
|
||||
} > 0
|
||||
}.also {
|
||||
if (it) {
|
||||
removeContent(id)
|
||||
contentDeletedBroadcastChannel.send(content)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun ResultRow.asRegisteredContent(): RegisteredContent = RegisteredContent(
|
||||
get(idColumn),
|
||||
Json.parse(Content.serializer(), get(dataColumn))
|
||||
)
|
||||
private fun ResultRow.asRegisteredContent(content: Content): RegisteredContent? = get(idColumn).let {
|
||||
RegisteredContent(
|
||||
it,
|
||||
content
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun getContentsIds(): Set<ContentId> {
|
||||
return transaction(database) {
|
||||
@@ -67,15 +109,16 @@ private class ContentAPIDatabaseTable(
|
||||
}.toSet()
|
||||
}
|
||||
override suspend fun getContentById(id: ContentId): RegisteredContent? {
|
||||
val content = getContent(id) ?: return null
|
||||
return transaction(database) {
|
||||
select { idColumn.eq(id) }.firstOrNull() ?.asRegisteredContent()
|
||||
select { idColumn.eq(id) }.limit(1).firstOrNull() ?.asRegisteredContent(content)
|
||||
}
|
||||
}
|
||||
override suspend fun getContentByPagination(pagination: Pagination): PaginationResult<out RegisteredContent> {
|
||||
return transaction(database) {
|
||||
selectAll().count() to selectAll().paginate(pagination).map { it.asRegisteredContent() }
|
||||
selectAll().count() to selectAll().paginate(pagination).map { it[idColumn] }
|
||||
}.let { (count, results) ->
|
||||
results.createPaginationResult(
|
||||
results.mapNotNull { RegisteredContent(it, getContent(it) ?: return@mapNotNull null) }.createPaginationResult(
|
||||
pagination,
|
||||
count
|
||||
)
|
||||
@@ -84,5 +127,8 @@ private class ContentAPIDatabaseTable(
|
||||
}
|
||||
|
||||
class ExposedContentAPI (
|
||||
database: Database
|
||||
) : ContentAPI by ContentAPIDatabaseTable(database)
|
||||
database: Database,
|
||||
textHolder: ContentHolderRepo<TextContent> = TextContentHolderRepo(database),
|
||||
binaryHolder: ContentHolderRepo<BinaryContent> = BinaryContentHolderRepo(database),
|
||||
specialHolder: ContentHolderRepo<SpecialContent> = SpecialContentHolderRepo(database)
|
||||
) : ContentAPI by ContentAPIDatabaseTable(database, textHolder, binaryHolder, specialHolder)
|
||||
|
@@ -0,0 +1,57 @@
|
||||
package com.insanusmokrassar.postssystem.core.exposed.content
|
||||
|
||||
import com.insanusmokrassar.postssystem.core.content.*
|
||||
import org.jetbrains.exposed.sql.*
|
||||
import org.jetbrains.exposed.sql.statements.api.ExposedBlob
|
||||
import org.jetbrains.exposed.sql.transactions.transaction
|
||||
|
||||
private class BinaryContentHolderRepoTable(
|
||||
private val database: Database
|
||||
) : ContentHolderRepo<BinaryContent>, Table() {
|
||||
private val idColumn = text("id")
|
||||
private val dataColumn = blob("data")
|
||||
private val mimeColumn = text("mimeType")
|
||||
private val originalFileNameColumn = text("filename")
|
||||
override val primaryKey: PrimaryKey = PrimaryKey(idColumn)
|
||||
|
||||
init {
|
||||
transaction(database) {
|
||||
SchemaUtils.createMissingTablesAndColumns(this@BinaryContentHolderRepoTable)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getContent(id: ContentId): BinaryContent? = transaction(database) {
|
||||
select {
|
||||
idColumn.eq(id)
|
||||
}.limit(1).firstOrNull() ?.let {
|
||||
val bytes = it[dataColumn].bytes
|
||||
BinaryContent(
|
||||
it[mimeColumn],
|
||||
it[originalFileNameColumn]
|
||||
) {
|
||||
bytes
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun removeContent(id: ContentId) {
|
||||
transaction(database) {
|
||||
deleteWhere { idColumn.eq(id) }
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun putContent(id: ContentId, content: BinaryContent) {
|
||||
transaction(database) {
|
||||
insert {
|
||||
it[idColumn] = id
|
||||
it[originalFileNameColumn] = content.originalFileName
|
||||
it[mimeColumn] = content.mimeType
|
||||
it[dataColumn] = ExposedBlob(content.dataAllocator())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class BinaryContentHolderRepo(
|
||||
database: Database
|
||||
) : ContentHolderRepo<BinaryContent> by BinaryContentHolderRepoTable(database)
|
@@ -0,0 +1,10 @@
|
||||
package com.insanusmokrassar.postssystem.core.exposed.content
|
||||
|
||||
import com.insanusmokrassar.postssystem.core.content.Content
|
||||
import com.insanusmokrassar.postssystem.core.content.ContentId
|
||||
|
||||
interface ContentHolderRepo<T : Content> {
|
||||
suspend fun getContent(id: ContentId) : T?
|
||||
suspend fun removeContent(id: ContentId)
|
||||
suspend fun putContent(id: ContentId, content: T)
|
||||
}
|
@@ -0,0 +1,46 @@
|
||||
package com.insanusmokrassar.postssystem.core.exposed.content
|
||||
|
||||
import com.insanusmokrassar.postssystem.core.content.*
|
||||
import org.jetbrains.exposed.sql.*
|
||||
import org.jetbrains.exposed.sql.transactions.transaction
|
||||
|
||||
private class SpecialContentHolderRepoTable(
|
||||
private val database: Database
|
||||
) : ContentHolderRepo<SpecialContent>, Table() {
|
||||
private val idColumn = text("id")
|
||||
private val internalIdColumn = text("internalId")
|
||||
override val primaryKey: PrimaryKey = PrimaryKey(idColumn)
|
||||
|
||||
init {
|
||||
transaction(database) {
|
||||
SchemaUtils.createMissingTablesAndColumns(this@SpecialContentHolderRepoTable)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getContent(id: ContentId): SpecialContent? = transaction(database) {
|
||||
select {
|
||||
idColumn.eq(id)
|
||||
}.limit(1).firstOrNull() ?.get(internalIdColumn) ?.let {
|
||||
SpecialContent(it)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun removeContent(id: ContentId) {
|
||||
transaction(database) {
|
||||
deleteWhere { idColumn.eq(id) }
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun putContent(id: ContentId, content: SpecialContent) {
|
||||
transaction(database) {
|
||||
insert {
|
||||
it[idColumn] = id
|
||||
it[internalIdColumn] = content.internalId
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class SpecialContentHolderRepo(
|
||||
database: Database
|
||||
) : ContentHolderRepo<SpecialContent> by SpecialContentHolderRepoTable(database)
|
@@ -0,0 +1,47 @@
|
||||
package com.insanusmokrassar.postssystem.core.exposed.content
|
||||
|
||||
import com.insanusmokrassar.postssystem.core.content.ContentId
|
||||
import com.insanusmokrassar.postssystem.core.content.TextContent
|
||||
import org.jetbrains.exposed.sql.*
|
||||
import org.jetbrains.exposed.sql.transactions.transaction
|
||||
|
||||
private class TextContentHolderRepoTable(
|
||||
private val database: Database
|
||||
) : ContentHolderRepo<TextContent>, Table() {
|
||||
private val idColumn = text("id")
|
||||
private val textColumn = text("text")
|
||||
override val primaryKey: PrimaryKey = PrimaryKey(idColumn)
|
||||
|
||||
init {
|
||||
transaction(database) {
|
||||
SchemaUtils.createMissingTablesAndColumns(this@TextContentHolderRepoTable)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getContent(id: ContentId): TextContent? = transaction(database) {
|
||||
select {
|
||||
idColumn.eq(id)
|
||||
}.limit(1).firstOrNull() ?.get(textColumn) ?.let {
|
||||
TextContent(it)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun removeContent(id: ContentId) {
|
||||
transaction(database) {
|
||||
deleteWhere { idColumn.eq(id) }
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun putContent(id: ContentId, content: TextContent) {
|
||||
transaction(database) {
|
||||
insert {
|
||||
it[idColumn] = id
|
||||
it[textColumn] = content.text
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class TextContentHolderRepo(
|
||||
database: Database
|
||||
) : ContentHolderRepo<TextContent> by TextContentHolderRepoTable(database)
|
@@ -1,12 +1,13 @@
|
||||
package com.insanusmokrassar.postssystem.core.exposed
|
||||
|
||||
import com.insanusmokrassar.postssystem.core.content.SimpleTextContent
|
||||
import com.insanusmokrassar.postssystem.core.content.TextContent
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.jetbrains.exposed.sql.Database
|
||||
import org.jetbrains.exposed.sql.transactions.transactionManager
|
||||
import java.io.File
|
||||
import java.sql.Connection
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
class ExposedContentAPICommonTests {
|
||||
private val tempFolder = System.getProperty("java.io.tmpdir")!!
|
||||
@@ -20,6 +21,10 @@ class ExposedContentAPICommonTests {
|
||||
}
|
||||
|
||||
val apis = databaseFiles.map {
|
||||
File(it).also {
|
||||
it.delete()
|
||||
it.deleteOnExit()
|
||||
}
|
||||
ExposedContentAPI(
|
||||
Database.Companion.connect("jdbc:sqlite:$it", driver = "org.sqlite.JDBC").also {
|
||||
it.transactionManager.defaultIsolationLevel = Connection.TRANSACTION_SERIALIZABLE
|
||||
@@ -28,9 +33,10 @@ class ExposedContentAPICommonTests {
|
||||
}
|
||||
|
||||
val results = apis.mapIndexed { i, api ->
|
||||
val content = runBlocking { api.createContent(SimpleTextContent(i.toString())) }
|
||||
val content = runBlocking { api.registerContent(TextContent(i.toString())) }
|
||||
assert(content != null)
|
||||
assert(runBlocking { api.getContentsIds().size == 1 })
|
||||
val ids = runBlocking { api.getContentsIds() }
|
||||
assertEquals(ids.size, 1)
|
||||
content!!
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user