0.4.0
This commit is contained in:
parent
e07a910039
commit
e83c2b4e0b
@ -8,4 +8,4 @@ uuidVersion=0.1.0
|
|||||||
|
|
||||||
gradle_bintray_plugin_version=1.8.4
|
gradle_bintray_plugin_version=1.8.4
|
||||||
|
|
||||||
core_version=0.3.0
|
core_version=0.4.0
|
||||||
|
@ -38,6 +38,7 @@ kotlin {
|
|||||||
dependencies {
|
dependencies {
|
||||||
implementation kotlin('stdlib')
|
implementation kotlin('stdlib')
|
||||||
api "org.jetbrains.kotlinx:kotlinx-coroutines-core-common:$kotlin_coroutines_version"
|
api "org.jetbrains.kotlinx:kotlinx-coroutines-core-common:$kotlin_coroutines_version"
|
||||||
|
// api "org.jetbrains.kotlinx:kotlinx-coroutines-io:$kotlin_io_version"
|
||||||
api "org.jetbrains.kotlinx:kotlinx-serialization-runtime-common:$kotlin_serialisation_runtime_version"
|
api "org.jetbrains.kotlinx:kotlinx-serialization-runtime-common:$kotlin_serialisation_runtime_version"
|
||||||
|
|
||||||
api "com.soywiz.korlibs.klock:klock:$klockVersion"
|
api "com.soywiz.korlibs.klock:klock:$klockVersion"
|
||||||
|
1
postssystem.core/gradle.properties
Normal file
1
postssystem.core/gradle.properties
Normal file
@ -0,0 +1 @@
|
|||||||
|
kotlin_io_version=0.27.0-eap13
|
@ -1,5 +1,7 @@
|
|||||||
package com.insanusmokrassar.postssystem.core.content
|
package com.insanusmokrassar.postssystem.core.content
|
||||||
|
|
||||||
|
import com.insanusmokrassar.postssystem.core.utils.ByteArrayAllocator
|
||||||
|
import com.insanusmokrassar.postssystem.core.utils.ByteArrayAllocatorSerializer
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
typealias ContentId = String
|
typealias ContentId = String
|
||||||
@ -11,15 +13,23 @@ typealias ContentId = String
|
|||||||
sealed class Content
|
sealed class Content
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class SimpleSpecialContent(
|
data class SpecialContent(
|
||||||
val internalId: ContentId
|
val internalId: ContentId
|
||||||
) : Content()
|
) : Content()
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class SimpleTextContent(
|
data class TextContent(
|
||||||
val text: String
|
val text: String
|
||||||
) : Content()
|
) : Content()
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class BinaryContent(
|
||||||
|
val mimeType: String,
|
||||||
|
val originalFileName: String,
|
||||||
|
@Serializable(ByteArrayAllocatorSerializer::class)
|
||||||
|
val dataAllocator: ByteArrayAllocator
|
||||||
|
) : Content()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Content which is already registered in database. Using its [id] you can retrieve all known
|
* Content which is already registered in database. Using its [id] you can retrieve all known
|
||||||
* [com.insanusmokrassar.postssystem.core.post.RegisteredPost]s by using
|
* [com.insanusmokrassar.postssystem.core.post.RegisteredPost]s by using
|
||||||
|
@ -3,6 +3,6 @@ package com.insanusmokrassar.postssystem.core.content.api
|
|||||||
import com.insanusmokrassar.postssystem.core.content.*
|
import com.insanusmokrassar.postssystem.core.content.*
|
||||||
|
|
||||||
interface WriteContentAPI {
|
interface WriteContentAPI {
|
||||||
suspend fun createContent(content: Content): RegisteredContent?
|
suspend fun registerContent(content: Content): RegisteredContent?
|
||||||
suspend fun deleteContent(id: ContentId): Boolean
|
suspend fun deleteContent(id: ContentId): Boolean
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,20 @@
|
|||||||
|
package com.insanusmokrassar.postssystem.core.utils
|
||||||
|
|
||||||
|
import kotlinx.serialization.*
|
||||||
|
import kotlinx.serialization.builtins.*
|
||||||
|
|
||||||
|
typealias ByteArrayAllocator = () -> ByteArray
|
||||||
|
|
||||||
|
object ByteArrayAllocatorSerializer : KSerializer<ByteArrayAllocator> {
|
||||||
|
private val realSerializer = ByteArraySerializer()
|
||||||
|
override val descriptor: SerialDescriptor = realSerializer.descriptor
|
||||||
|
|
||||||
|
override fun deserialize(decoder: Decoder): ByteArrayAllocator {
|
||||||
|
val bytes = decoder.decode(realSerializer)
|
||||||
|
return { bytes }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun serialize(encoder: Encoder, value: ByteArrayAllocator) {
|
||||||
|
encoder.encodeSerializableValue(realSerializer, value())
|
||||||
|
}
|
||||||
|
}
|
@ -13,9 +13,9 @@ class ContentSerialization {
|
|||||||
@Test
|
@Test
|
||||||
fun test_that_content_correctly_serializing_and_deserializing() {
|
fun test_that_content_correctly_serializing_and_deserializing() {
|
||||||
val contents = (0 until simpleTextTestEntries).map {
|
val contents = (0 until simpleTextTestEntries).map {
|
||||||
SimpleTextContent("Example$it")
|
TextContent("Example$it")
|
||||||
} + (0 until simpleSpecialTestEntries).map {
|
} + (0 until simpleSpecialTestEntries).map {
|
||||||
SimpleSpecialContent("$it")
|
SpecialContent("$it")
|
||||||
}
|
}
|
||||||
|
|
||||||
val registeredContentFakes = contents.map { content ->
|
val registeredContentFakes = contents.map { content ->
|
||||||
|
@ -1,56 +0,0 @@
|
|||||||
package com.insanusmokrassar.postssystem.core.api
|
|
||||||
|
|
||||||
import com.insanusmokrassar.postssystem.core.content.*
|
|
||||||
import com.insanusmokrassar.postssystem.core.content.api.ContentAPI
|
|
||||||
import com.insanusmokrassar.postssystem.core.utils.generateContentId
|
|
||||||
import com.insanusmokrassar.postssystem.core.utils.pagination.*
|
|
||||||
import kotlinx.coroutines.channels.BroadcastChannel
|
|
||||||
import kotlinx.coroutines.channels.Channel
|
|
||||||
import kotlinx.coroutines.flow.Flow
|
|
||||||
import kotlinx.coroutines.flow.asFlow
|
|
||||||
import kotlinx.serialization.ImplicitReflectionSerializer
|
|
||||||
|
|
||||||
@ImplicitReflectionSerializer
|
|
||||||
class InMemoryContentAPI(
|
|
||||||
initialContent: List<RegisteredContent> = emptyList()
|
|
||||||
): ContentAPI {
|
|
||||||
private val contentCreatedBroadcastChannel = BroadcastChannel<RegisteredContent>(Channel.BUFFERED)
|
|
||||||
private val contentDeletedBroadcastChannel = BroadcastChannel<RegisteredContent>(Channel.BUFFERED)
|
|
||||||
|
|
||||||
override val contentCreatedFlow: Flow<RegisteredContent>
|
|
||||||
get() = contentCreatedBroadcastChannel.asFlow()
|
|
||||||
|
|
||||||
override val contentDeletedFlow: Flow<RegisteredContent>
|
|
||||||
get() = contentDeletedBroadcastChannel.asFlow()
|
|
||||||
|
|
||||||
private val contents: MutableMap<ContentId, RegisteredContent> = initialContent
|
|
||||||
.associateBy(RegisteredContent::id)
|
|
||||||
.toMutableMap()
|
|
||||||
|
|
||||||
override suspend fun createContent(content: Content): RegisteredContent? {
|
|
||||||
return RegisteredContent(
|
|
||||||
generateContentId(),
|
|
||||||
content
|
|
||||||
).also { registeredContent ->
|
|
||||||
contents[registeredContent.id] = registeredContent
|
|
||||||
contentCreatedBroadcastChannel.send(registeredContent)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun getContentsIds(): Set<ContentId> = contents.keys.toSet()
|
|
||||||
|
|
||||||
override suspend fun getContentById(id: ContentId): RegisteredContent? = contents[id]
|
|
||||||
|
|
||||||
override suspend fun getContentByPagination(pagination: Pagination): PaginationResult<out RegisteredContent> {
|
|
||||||
return contents.values.asSequence().drop(pagination.firstIndex).take(pagination.size).toList().createPaginationResult(
|
|
||||||
pagination,
|
|
||||||
commonObjectsNumber = contents.size.toLong()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun deleteContent(id: ContentId): Boolean {
|
|
||||||
return contents.remove(id)?.also { content ->
|
|
||||||
contentDeletedBroadcastChannel.send(content)
|
|
||||||
} != null
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,86 +0,0 @@
|
|||||||
package com.insanusmokrassar.postssystem.core.api
|
|
||||||
|
|
||||||
import com.insanusmokrassar.postssystem.core.content.ContentId
|
|
||||||
import com.insanusmokrassar.postssystem.core.post.*
|
|
||||||
import com.insanusmokrassar.postssystem.core.post.api.PostsAPI
|
|
||||||
import com.insanusmokrassar.postssystem.core.utils.generatePostId
|
|
||||||
import com.insanusmokrassar.postssystem.core.utils.pagination.*
|
|
||||||
import com.soywiz.klock.DateTime
|
|
||||||
import kotlinx.coroutines.channels.BroadcastChannel
|
|
||||||
import kotlinx.coroutines.channels.Channel.Factory.BUFFERED
|
|
||||||
import kotlinx.coroutines.flow.Flow
|
|
||||||
import kotlinx.coroutines.flow.asFlow
|
|
||||||
import kotlinx.serialization.ImplicitReflectionSerializer
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Thread-unsafe sample realization of [PostsAPI]
|
|
||||||
*/
|
|
||||||
@ImplicitReflectionSerializer
|
|
||||||
class InMemoryPostsAPI(
|
|
||||||
initialPosts: List<RegisteredPost> = emptyList()
|
|
||||||
) : PostsAPI {
|
|
||||||
private val posts: MutableMap<PostId, RegisteredPost> = initialPosts.associateBy { it.id }.toMutableMap()
|
|
||||||
private val sortedByDatePosts: List<RegisteredPost>
|
|
||||||
get() = posts.values.sortedBy { it.creationDate }
|
|
||||||
|
|
||||||
|
|
||||||
private val postCreatedBroadcastChannel = BroadcastChannel<RegisteredPost>(BUFFERED)
|
|
||||||
override val postCreatedFlow: Flow<RegisteredPost> = postCreatedBroadcastChannel.asFlow()
|
|
||||||
|
|
||||||
private val postDeletedBroadcastChannel = BroadcastChannel<RegisteredPost>(BUFFERED)
|
|
||||||
override val postDeletedFlow: Flow<RegisteredPost> = postDeletedBroadcastChannel.asFlow()
|
|
||||||
|
|
||||||
private val postUpdatedBroadcastChannel = BroadcastChannel<RegisteredPost>(BUFFERED)
|
|
||||||
override val postUpdatedFlow: Flow<RegisteredPost> = postUpdatedBroadcastChannel.asFlow()
|
|
||||||
|
|
||||||
|
|
||||||
override suspend fun createPost(post: Post): RegisteredPost? {
|
|
||||||
return SimpleRegisteredPost(
|
|
||||||
generatePostId(),
|
|
||||||
post.content
|
|
||||||
).also { newPost ->
|
|
||||||
posts[newPost.id] = newPost
|
|
||||||
postCreatedBroadcastChannel.send(newPost)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun deletePost(id: PostId): Boolean {
|
|
||||||
return posts.remove(id)?.also {
|
|
||||||
postDeletedBroadcastChannel.send(it)
|
|
||||||
} != null
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun updatePostContent(postId: PostId, post: Post): Boolean {
|
|
||||||
return getPostById(postId)?.also { dbPost ->
|
|
||||||
val newPost = SimpleRegisteredPost(
|
|
||||||
dbPost.id,
|
|
||||||
post.content,
|
|
||||||
dbPost.creationDate
|
|
||||||
)
|
|
||||||
posts[newPost.id] = newPost
|
|
||||||
postUpdatedBroadcastChannel.send(newPost)
|
|
||||||
} != null
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun getPostsIds(): Set<PostId> = posts.keys.toSet()
|
|
||||||
override suspend fun getPostById(id: PostId): RegisteredPost? = posts[id]
|
|
||||||
override suspend fun getPostsByContent(id: ContentId): List<RegisteredPost> =
|
|
||||||
posts.values.filter { post -> id in post.content }
|
|
||||||
|
|
||||||
override suspend fun getPostsByCreatingDates(from: DateTime, to: DateTime): List<RegisteredPost> =
|
|
||||||
(from .. to).let { range ->
|
|
||||||
posts.values.filter {
|
|
||||||
it.creationDate in range
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun getPostsByPagination(
|
|
||||||
pagination: Pagination
|
|
||||||
): PaginationResult<RegisteredPost> = sortedByDatePosts.subList(
|
|
||||||
pagination.firstIndex,
|
|
||||||
pagination.lastIndex
|
|
||||||
).createPaginationResult(
|
|
||||||
pagination,
|
|
||||||
commonObjectsNumber = posts.size.toLong()
|
|
||||||
)
|
|
||||||
}
|
|
@ -2,20 +2,30 @@ package com.insanusmokrassar.postssystem.core.exposed
|
|||||||
|
|
||||||
import com.insanusmokrassar.postssystem.core.content.*
|
import com.insanusmokrassar.postssystem.core.content.*
|
||||||
import com.insanusmokrassar.postssystem.core.content.api.ContentAPI
|
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.generateContentId
|
||||||
import com.insanusmokrassar.postssystem.core.utils.pagination.*
|
import com.insanusmokrassar.postssystem.core.utils.pagination.*
|
||||||
import kotlinx.coroutines.channels.BroadcastChannel
|
import kotlinx.coroutines.channels.BroadcastChannel
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.asFlow
|
import kotlinx.coroutines.flow.asFlow
|
||||||
import kotlinx.serialization.json.Json
|
|
||||||
import org.jetbrains.exposed.sql.*
|
import org.jetbrains.exposed.sql.*
|
||||||
import org.jetbrains.exposed.sql.transactions.transaction
|
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 class ContentAPIDatabaseTable(
|
||||||
private val database: Database
|
private val database: Database,
|
||||||
) : Table("ContentAPI"), ContentAPI {
|
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 idColumn = text("_id")
|
||||||
internal val dataColumn = text("data")
|
internal val typeColumn = text("type")
|
||||||
|
|
||||||
init {
|
init {
|
||||||
transaction(database) {
|
transaction(database) {
|
||||||
@ -28,19 +38,48 @@ private class ContentAPIDatabaseTable(
|
|||||||
override val contentCreatedFlow: Flow<RegisteredContent> = contentCreatedBroadcastChannel.asFlow()
|
override val contentCreatedFlow: Flow<RegisteredContent> = contentCreatedBroadcastChannel.asFlow()
|
||||||
override val contentDeletedFlow: Flow<RegisteredContent> = contentDeletedBroadcastChannel.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) {
|
return transaction(database) {
|
||||||
insert {
|
insert {
|
||||||
it[idColumn] = generateContentId()
|
it[idColumn] = id
|
||||||
it[dataColumn] = Json.stringify(Content.serializer(), content)
|
it[typeColumn] = type
|
||||||
}.getOrNull(idColumn) ?.let { id ->
|
}.getOrNull(idColumn)
|
||||||
RegisteredContent(
|
} ?.let { id ->
|
||||||
id,
|
putContent(id, content)
|
||||||
content
|
RegisteredContent(
|
||||||
)
|
id,
|
||||||
}
|
content
|
||||||
|
)
|
||||||
} ?.also {
|
} ?.also {
|
||||||
contentCreatedBroadcastChannel.send(it)
|
contentCreatedBroadcastChannel.send(it)
|
||||||
|
} ?: null.also {
|
||||||
|
removeContent(id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
override suspend fun deleteContent(id: ContentId): Boolean {
|
override suspend fun deleteContent(id: ContentId): Boolean {
|
||||||
@ -51,15 +90,18 @@ private class ContentAPIDatabaseTable(
|
|||||||
} > 0
|
} > 0
|
||||||
}.also {
|
}.also {
|
||||||
if (it) {
|
if (it) {
|
||||||
|
removeContent(id)
|
||||||
contentDeletedBroadcastChannel.send(content)
|
contentDeletedBroadcastChannel.send(content)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun ResultRow.asRegisteredContent(): RegisteredContent = RegisteredContent(
|
private fun ResultRow.asRegisteredContent(content: Content): RegisteredContent? = get(idColumn).let {
|
||||||
get(idColumn),
|
RegisteredContent(
|
||||||
Json.parse(Content.serializer(), get(dataColumn))
|
it,
|
||||||
)
|
content
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
override suspend fun getContentsIds(): Set<ContentId> {
|
override suspend fun getContentsIds(): Set<ContentId> {
|
||||||
return transaction(database) {
|
return transaction(database) {
|
||||||
@ -67,15 +109,16 @@ private class ContentAPIDatabaseTable(
|
|||||||
}.toSet()
|
}.toSet()
|
||||||
}
|
}
|
||||||
override suspend fun getContentById(id: ContentId): RegisteredContent? {
|
override suspend fun getContentById(id: ContentId): RegisteredContent? {
|
||||||
|
val content = getContent(id) ?: return null
|
||||||
return transaction(database) {
|
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> {
|
override suspend fun getContentByPagination(pagination: Pagination): PaginationResult<out RegisteredContent> {
|
||||||
return transaction(database) {
|
return transaction(database) {
|
||||||
selectAll().count() to selectAll().paginate(pagination).map { it.asRegisteredContent() }
|
selectAll().count() to selectAll().paginate(pagination).map { it[idColumn] }
|
||||||
}.let { (count, results) ->
|
}.let { (count, results) ->
|
||||||
results.createPaginationResult(
|
results.mapNotNull { RegisteredContent(it, getContent(it) ?: return@mapNotNull null) }.createPaginationResult(
|
||||||
pagination,
|
pagination,
|
||||||
count
|
count
|
||||||
)
|
)
|
||||||
@ -84,5 +127,8 @@ private class ContentAPIDatabaseTable(
|
|||||||
}
|
}
|
||||||
|
|
||||||
class ExposedContentAPI (
|
class ExposedContentAPI (
|
||||||
database: Database
|
database: Database,
|
||||||
) : ContentAPI by ContentAPIDatabaseTable(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
|
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 kotlinx.coroutines.runBlocking
|
||||||
import org.jetbrains.exposed.sql.Database
|
import org.jetbrains.exposed.sql.Database
|
||||||
import org.jetbrains.exposed.sql.transactions.transactionManager
|
import org.jetbrains.exposed.sql.transactions.transactionManager
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.sql.Connection
|
import java.sql.Connection
|
||||||
import kotlin.test.Test
|
import kotlin.test.Test
|
||||||
|
import kotlin.test.assertEquals
|
||||||
|
|
||||||
class ExposedContentAPICommonTests {
|
class ExposedContentAPICommonTests {
|
||||||
private val tempFolder = System.getProperty("java.io.tmpdir")!!
|
private val tempFolder = System.getProperty("java.io.tmpdir")!!
|
||||||
@ -20,6 +21,10 @@ class ExposedContentAPICommonTests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val apis = databaseFiles.map {
|
val apis = databaseFiles.map {
|
||||||
|
File(it).also {
|
||||||
|
it.delete()
|
||||||
|
it.deleteOnExit()
|
||||||
|
}
|
||||||
ExposedContentAPI(
|
ExposedContentAPI(
|
||||||
Database.Companion.connect("jdbc:sqlite:$it", driver = "org.sqlite.JDBC").also {
|
Database.Companion.connect("jdbc:sqlite:$it", driver = "org.sqlite.JDBC").also {
|
||||||
it.transactionManager.defaultIsolationLevel = Connection.TRANSACTION_SERIALIZABLE
|
it.transactionManager.defaultIsolationLevel = Connection.TRANSACTION_SERIALIZABLE
|
||||||
@ -28,9 +33,10 @@ class ExposedContentAPICommonTests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val results = apis.mapIndexed { i, api ->
|
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(content != null)
|
||||||
assert(runBlocking { api.getContentsIds().size == 1 })
|
val ids = runBlocking { api.getContentsIds() }
|
||||||
|
assertEquals(ids.size, 1)
|
||||||
content!!
|
content!!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user