diff --git a/core/src/commonMain/kotlin/com/insanusmokrassar/postssystem/core/post/api/ReadPostsAPI.kt b/core/src/commonMain/kotlin/com/insanusmokrassar/postssystem/core/post/api/ReadPostsAPI.kt index f33cd66a..4d33e34e 100644 --- a/core/src/commonMain/kotlin/com/insanusmokrassar/postssystem/core/post/api/ReadPostsAPI.kt +++ b/core/src/commonMain/kotlin/com/insanusmokrassar/postssystem/core/post/api/ReadPostsAPI.kt @@ -29,9 +29,9 @@ interface ReadPostsAPI { suspend fun getPostsByContent(id: ContentId): List /** - * @return all [RegisteredPost]s between [from] and [to]. Range will be used INCLUSIVE, line \[[from], [to]\] + * @return all [RegisteredPost]s which was registered between [from] and [to]. Range will be used INCLUSIVE, line \[[from], [to]\] */ - suspend fun getPostsByDates(from: DateTime = MIN_DATE, to: DateTime = MAX_DATE): List + suspend fun getPostsByCreatingDates(from: DateTime = MIN_DATE, to: DateTime = MAX_DATE): List /** * @return all posts by pages basing on their creation date @@ -39,10 +39,10 @@ interface ReadPostsAPI { suspend fun getPostsByPagination(pagination: Pagination): PaginationResult } -suspend fun ReadPostsAPI.getPostsByDates( +suspend fun ReadPostsAPI.getPostsByCreatingDates( from: DateTime? = null, to: DateTime? = null -) = getPostsByDates( +) = getPostsByCreatingDates( from ?: MIN_DATE, to ?: MAX_DATE ) diff --git a/core/src/commonTest/kotlin/com/insanusmokrassar/postssystem/core/api/InMemoryPostsAPI.kt b/core/src/commonTest/kotlin/com/insanusmokrassar/postssystem/core/api/InMemoryPostsAPI.kt index 0a265175..5e5512dd 100644 --- a/core/src/commonTest/kotlin/com/insanusmokrassar/postssystem/core/api/InMemoryPostsAPI.kt +++ b/core/src/commonTest/kotlin/com/insanusmokrassar/postssystem/core/api/InMemoryPostsAPI.kt @@ -68,7 +68,7 @@ class InMemoryPostsAPI( override suspend fun getPostsByContent(id: ContentId): List = posts.values.filter { post -> id in post.content } - override suspend fun getPostsByDates(from: DateTime, to: DateTime): List = + override suspend fun getPostsByCreatingDates(from: DateTime, to: DateTime): List = (from .. to).let { range -> posts.values.filter { it.creationDate in range diff --git a/exposed/src/main/kotlin/com/insanusmokrassar/postssystem/core/exposed/ExposedContentAPI.kt b/exposed/src/main/kotlin/com/insanusmokrassar/postssystem/core/exposed/ExposedContentAPI.kt index 08a684ee..938db144 100644 --- a/exposed/src/main/kotlin/com/insanusmokrassar/postssystem/core/exposed/ExposedContentAPI.kt +++ b/exposed/src/main/kotlin/com/insanusmokrassar/postssystem/core/exposed/ExposedContentAPI.kt @@ -18,7 +18,7 @@ private class ContentAPIDatabaseTable( internal val idColumn = text("_id") internal val dataColumn = text("data") - private inline fun transaction(noinline body: Transaction.() -> T): T = transaction(database, body) + private inline fun transaction(noinline body: Transaction.() -> T): T = database.transaction(body) init { transaction { @@ -84,5 +84,5 @@ private class ContentAPIDatabaseTable( } class ExposedContentAPI ( - private val database: Database + database: Database ) : ContentAPI by ContentAPIDatabaseTable(database) diff --git a/exposed/src/main/kotlin/com/insanusmokrassar/postssystem/core/exposed/ExposedPostsAPI.kt b/exposed/src/main/kotlin/com/insanusmokrassar/postssystem/core/exposed/ExposedPostsAPI.kt new file mode 100644 index 00000000..ac50e1c7 --- /dev/null +++ b/exposed/src/main/kotlin/com/insanusmokrassar/postssystem/core/exposed/ExposedPostsAPI.kt @@ -0,0 +1,177 @@ +package com.insanusmokrassar.postssystem.core.exposed + +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 +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.asFlow +import org.jetbrains.exposed.sql.* + +private class PostsAPIContentRelations( + private val database: Database +) : Table() { + private val postIdColumn = text("postId") + private val contentIdColumn = text("contentId") + + private inline fun transaction(noinline body: Transaction.() -> T): T = database.transaction(body) + + init { + transaction { + SchemaUtils.createMissingTablesAndColumns(this@PostsAPIContentRelations) + } + } + + fun getPostContents(postId: PostId): List { + return transaction { + select { postIdColumn.eq(postId) }.map { it[contentIdColumn] } + } + } + + fun getContentPosts(contentId: ContentId): List { + return transaction { + select { contentIdColumn.eq(contentId) }.map { it[postIdColumn] } + } + } + + fun linkPostAndContents(postId: PostId, vararg contentIds: ContentId) { + transaction { + val leftToPut = contentIds.toSet() - getPostContents(postId) + leftToPut.forEach { contentId -> + insert { + it[postIdColumn] = postId + it[contentIdColumn] = contentId + } + } + } + } + fun unlinkPostAndContents(postId: PostId, vararg contentIds: ContentId): Boolean { + return transaction { + deleteWhere { + postIdColumn.eq(postId).and(contentIdColumn.inList(contentIds.toList())) + } > 0 + } + } +} + +private class PostsAPIDatabaseTable( + private val database: Database +) : PostsAPI, Table() { + private val contentsTable = PostsAPIContentRelations(database) + + private val idColumn = text("postId") + private val creationDateColumn = datetime("creationDate").default(org.joda.time.DateTime.now()) + private val modificationDateColumn = datetime("modificationDate").default(org.joda.time.DateTime.now()) + + + private val postCreatedBroadcastChannel = BroadcastChannel(Channel.BUFFERED) + override val postCreatedFlow: Flow = postCreatedBroadcastChannel.asFlow() + + private val postDeletedBroadcastChannel = BroadcastChannel(Channel.BUFFERED) + override val postDeletedFlow: Flow = postDeletedBroadcastChannel.asFlow() + + private val postUpdatedBroadcastChannel = BroadcastChannel(Channel.BUFFERED) + override val postUpdatedFlow: Flow = postUpdatedBroadcastChannel.asFlow() + + private inline fun transaction(noinline body: Transaction.() -> T): T = database.transaction(body) + + init { + transaction { + SchemaUtils.createMissingTablesAndColumns(this@PostsAPIDatabaseTable) + } + } + + private fun ResultRow.toRegisteredPost(): RegisteredPost = get(idColumn).let { id -> + SimpleRegisteredPost( + id, + contentsTable.getPostContents(id), + DateTime(get(creationDateColumn).millis), + DateTime(get(modificationDateColumn).millis) + ) + } + + override suspend fun createPost(post: Post): RegisteredPost? { + val id = generatePostId() + return transaction { + insert { + it[idColumn] = id + } + contentsTable.linkPostAndContents(id, *post.content.toTypedArray()) + select { idColumn.eq(id) }.firstOrNull() ?.toRegisteredPost() + } ?.also { + postCreatedBroadcastChannel.send(it) + } + } + + override suspend fun deletePost(id: PostId): Boolean { + val post = getPostById(id) ?: return false + return (transaction { + deleteWhere { idColumn.eq(id) } + } > 0).also { + if (it) { + postDeletedBroadcastChannel.send(post) + contentsTable.unlinkPostAndContents(id, *post.content.toTypedArray()) + } + } + } + + override suspend fun updatePostContent(postId: PostId, post: Post): Boolean { + return transaction { + val alreadyLinked = contentsTable.getPostContents(postId) + val toRemove = alreadyLinked - post.content + val toInsert = post.content - alreadyLinked + val updated = (toRemove.isNotEmpty() && contentsTable.unlinkPostAndContents(postId, *toRemove.toTypedArray())) || toInsert.isNotEmpty() + if (toInsert.isNotEmpty()) { + contentsTable.linkPostAndContents(postId, *toInsert.toTypedArray()) + } + updated + }.also { + if (it) { + getPostById(postId) ?.also { updatedPost -> postUpdatedBroadcastChannel.send(updatedPost) } + } + } + } + override suspend fun getPostsIds(): Set { + return transaction { + selectAll().map { it[idColumn] }.toSet() + } + } + + override suspend fun getPostById(id: PostId): RegisteredPost? { + return transaction { + select { idColumn.eq(id) }.firstOrNull() ?.toRegisteredPost() + } + } + + override suspend fun getPostsByContent(id: ContentId): List { + return transaction { + val postsIds = contentsTable.getContentPosts(id) + select { idColumn.inList(postsIds) }.map { it.toRegisteredPost() } + } + } + + override suspend fun getPostsByCreatingDates(from: DateTime, to: DateTime): List { + return transaction { + select { creationDateColumn.between(from, to) }.map { it.toRegisteredPost() } + } + } + + override suspend fun getPostsByPagination(pagination: Pagination): PaginationResult { + return transaction { + val posts = selectAll().limit(pagination.size, pagination.firstIndex).orderBy(creationDateColumn).map { + it.toRegisteredPost() + } + val postsNumber = selectAll().count() + pagination.createResult(postsNumber, posts) + } + } + +} + +class ExposedPostsAPI ( + database: Database +) : PostsAPI by PostsAPIDatabaseTable(database) diff --git a/exposed/src/main/kotlin/com/insanusmokrassar/postssystem/core/exposed/ExposedUtils.kt b/exposed/src/main/kotlin/com/insanusmokrassar/postssystem/core/exposed/ExposedUtils.kt new file mode 100644 index 00000000..6e0de410 --- /dev/null +++ b/exposed/src/main/kotlin/com/insanusmokrassar/postssystem/core/exposed/ExposedUtils.kt @@ -0,0 +1,6 @@ +package com.insanusmokrassar.postssystem.core.exposed + +import org.jetbrains.exposed.sql.Database +import org.jetbrains.exposed.sql.Transaction + +inline fun Database.transaction(noinline body: Transaction.() -> T): T = org.jetbrains.exposed.sql.transactions.transaction(this, body)