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 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) ) } 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)