Skip to content

Commit

Permalink
Merge pull request #8 from groupeminaste/feature/posts
Browse files Browse the repository at this point in the history
Posts
  • Loading branch information
nathanfallet authored Dec 10, 2023
2 parents 5cfd6c2 + 7c67416 commit d169b24
Show file tree
Hide file tree
Showing 18 changed files with 1,085 additions and 81 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,37 +4,68 @@ import io.ktor.http.*
import io.ktor.server.application.*
import me.nathanfallet.extopy.models.posts.Post
import me.nathanfallet.extopy.models.posts.PostPayload
import me.nathanfallet.extopy.models.users.User
import me.nathanfallet.extopy.models.users.UserContext
import me.nathanfallet.ktorx.controllers.IModelController
import me.nathanfallet.ktorx.models.exceptions.ControllerException
import me.nathanfallet.ktorx.usecases.users.IRequireUserForCallUseCase
import me.nathanfallet.usecases.models.create.ICreateModelSuspendUseCase
import me.nathanfallet.usecases.models.create.context.ICreateModelWithContextSuspendUseCase
import me.nathanfallet.usecases.models.delete.IDeleteModelSuspendUseCase
import me.nathanfallet.usecases.models.get.context.IGetModelWithContextSuspendUseCase
import me.nathanfallet.usecases.models.update.IUpdateModelSuspendUseCase

class PostsController(
private val requireUserForCallUseCase: IRequireUserForCallUseCase,
private val createPostUseCase: ICreateModelSuspendUseCase<Post, PostPayload>,
private val createPostUseCase: ICreateModelWithContextSuspendUseCase<Post, PostPayload>,
private val getPostUseCase: IGetModelWithContextSuspendUseCase<Post, String>,
private val updatePostUseCase: IUpdateModelSuspendUseCase<Post, String, PostPayload>,
private val deletePostUseCase: IDeleteModelSuspendUseCase<Post, String>,
) : IModelController<Post, String, PostPayload, PostPayload> {

override suspend fun list(call: ApplicationCall): List<Post> {
throw ControllerException(HttpStatusCode.MethodNotAllowed, "posts_list_not_allowed")
}

override suspend fun create(call: ApplicationCall, payload: PostPayload): Post {
val user = requireUserForCallUseCase(call)
return createPostUseCase(payload) ?: throw ControllerException(
val user = requireUserForCallUseCase(call) as User
return createPostUseCase(payload, UserContext(user.id)) ?: throw ControllerException(
HttpStatusCode.InternalServerError, "error_internal"
)
}

override suspend fun delete(call: ApplicationCall, id: String) {
TODO("Not yet implemented")
}

override suspend fun get(call: ApplicationCall, id: String): Post {
TODO("Not yet implemented")
val user = requireUserForCallUseCase(call) as User
return getPostUseCase(id, UserContext(user.id)) ?: throw ControllerException(
HttpStatusCode.NotFound, "posts_not_found"
)
}

override suspend fun list(call: ApplicationCall): List<Post> {
TODO("Not yet implemented")
override suspend fun update(call: ApplicationCall, id: String, payload: PostPayload): Post {
val user = requireUserForCallUseCase(call) as User
val post = getPostUseCase(id, UserContext(user.id)) ?: throw ControllerException(
HttpStatusCode.NotFound, "posts_not_found"
)
if (post.userId != user.id) throw ControllerException(
HttpStatusCode.Forbidden, "posts_update_not_allowed"
)
return updatePostUseCase(
id, payload
) ?: throw ControllerException(
HttpStatusCode.InternalServerError, "error_internal"
)
}

override suspend fun update(call: ApplicationCall, id: String, payload: PostPayload): Post {
TODO("Not yet implemented")
override suspend fun delete(call: ApplicationCall, id: String) {
val user = requireUserForCallUseCase(call) as User
val post = getPostUseCase(id, UserContext(user.id)) ?: throw ControllerException(
HttpStatusCode.NotFound, "posts_not_found"
)
if (post.userId != user.id) throw ControllerException(
HttpStatusCode.Forbidden, "posts_delete_not_allowed"
)
if (!deletePostUseCase(id)) throw ControllerException(
HttpStatusCode.InternalServerError, "error_internal"
)
}

}
Original file line number Diff line number Diff line change
@@ -1,4 +1,20 @@
package me.nathanfallet.extopy.controllers.posts

class PostsRouter {
}
import me.nathanfallet.extopy.models.posts.Post
import me.nathanfallet.extopy.models.posts.PostPayload
import me.nathanfallet.ktorx.controllers.IModelController
import me.nathanfallet.ktorx.models.api.APIMapping
import me.nathanfallet.ktorx.routers.api.APIModelRouter

class PostsRouter(
postsController: IModelController<Post, String, PostPayload, PostPayload>,
) : APIModelRouter<Post, String, PostPayload, PostPayload>(
Post::class,
PostPayload::class,
PostPayload::class,
postsController,
mapping = APIMapping(
listEnabled = false
),
prefix = "/api/v1"
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package me.nathanfallet.extopy.database.posts

import kotlinx.datetime.Clock
import me.nathanfallet.extopy.database.Database
import me.nathanfallet.extopy.database.users.Users
import me.nathanfallet.extopy.models.posts.Post
import me.nathanfallet.extopy.models.posts.PostPayload
import me.nathanfallet.extopy.models.users.UserContext
import me.nathanfallet.extopy.repositories.posts.IPostsRepository
import me.nathanfallet.usecases.context.IContext
import org.jetbrains.exposed.sql.*

class DatabasePostsRepository(
private val database: Database,
) : IPostsRepository {

override suspend fun get(id: String, context: IContext?): Post? {
if (context !is UserContext) return null
return database.dbQuery {
customJoin(context.userId)
.select { Posts.id eq id }
.groupBy(Posts.id)
.map { Posts.toPost(it, Users.toUser(it)) }
.singleOrNull()
}
}

override suspend fun create(payload: PostPayload, context: IContext?): Post? {
if (context !is UserContext) return null
val id = database.dbQuery {
val id = Posts.generateId()
Posts.insert {
it[Posts.id] = id
it[userId] = context.userId
it[repliedToId] = payload.repliedToId
it[repostOfId] = payload.repostOfId
it[body] = payload.body
it[published] = Clock.System.now().toString()
it[edited] = null
it[expiration] = Clock.System.now().toString()
it[visibility] = ""
}
id
}
return get(id, context)
}

override suspend fun update(id: String, payload: PostPayload, context: IContext?): Boolean {
return database.dbQuery {
Posts.update({ Posts.id eq id }) {
it[body] = payload.body
it[edited] = Clock.System.now().toString()
}
} == 1
}

override suspend fun delete(id: String, context: IContext?): Boolean {
return database.dbQuery {
Posts.deleteWhere {
Op.build { Posts.id eq id }
}
} == 1
}

private fun customJoin(viewedBy: String): FieldSet {
return customJoinColumnSet(viewedBy).customPostsSlice()
}

private fun customJoinColumnSet(viewedBy: String): ColumnSet {
return Posts.join(Users, JoinType.INNER, Posts.userId, Users.id)
.join(LikesInPosts, JoinType.LEFT, Posts.id, LikesInPosts.postId)
.join(Posts.replies, JoinType.LEFT, Posts.id, Posts.replies[Posts.repliedToId])
.join(Posts.reposts, JoinType.LEFT, Posts.id, Posts.reposts[Posts.repostOfId])
.join(
LikesInPosts.likesIn,
JoinType.LEFT,
Posts.id,
LikesInPosts.likesIn[LikesInPosts.postId]
) { LikesInPosts.likesIn[LikesInPosts.userId] eq viewedBy }
}

private fun ColumnSet.customPostsSlice(additionalFields: List<Expression<*>> = listOf()): FieldSet {
return slice(
Posts.columns +
Users.id +
Users.displayName +
Users.username +
Users.avatar +
Users.verified +
Posts.likesCount +
Posts.repliesCount +
Posts.repostsCount +
Posts.likesIn +
additionalFields
)
}

}
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package me.nathanfallet.extopy.database.posts

import kotlinx.datetime.toInstant
import me.nathanfallet.extopy.database.users.Users
import me.nathanfallet.extopy.extensions.generateId
import me.nathanfallet.extopy.models.posts.Post
import me.nathanfallet.extopy.models.users.User
Expand All @@ -10,7 +9,7 @@ import org.jetbrains.exposed.sql.*
object Posts : Table() {

val id = varchar("id", 32)
val userId = varchar("user_id", 32)
val userId = varchar("user_id", 32).index()
val repliedToId = varchar("replied_to_id", 32).nullable()
val repostOfId = varchar("repost_of_id", 32).nullable()
val body = text("body")
Expand Down Expand Up @@ -41,7 +40,7 @@ object Posts : Table() {

fun toPost(
row: ResultRow,
user: User?,
user: User? = null,
) = Post(
row[id],
row.getOrNull(userId),
Expand All @@ -59,51 +58,4 @@ object Posts : Table() {
row.getOrNull(likesIn)?.let { it >= 1L }
)

fun delete(id: String) {
LikesInPosts.deleteWhere {
Op.build { postId eq id }
}
Posts.deleteWhere {
Op.build { Posts.id eq id }
}
Posts.select {
repliedToId eq id or (repostOfId eq id)
}.forEach {
delete(it[Posts.id])
}
}

fun customJoinnable(viewedBy: String): ColumnSet {
return Posts.join(Users, JoinType.INNER, userId, Users.id)
.join(LikesInPosts, JoinType.LEFT, id, LikesInPosts.postId)
.join(replies, JoinType.LEFT, id, replies[repliedToId])
.join(reposts, JoinType.LEFT, id, reposts[repostOfId])
.join(
LikesInPosts.likesIn,
JoinType.LEFT,
id,
LikesInPosts.likesIn[LikesInPosts.postId],
{ LikesInPosts.likesIn[LikesInPosts.userId] eq viewedBy }
)
}

fun customJoin(viewedBy: String): FieldSet {
return customJoinnable(viewedBy).customPostsSlice()
}
}

fun ColumnSet.customPostsSlice(additionalFields: List<Expression<*>> = listOf()): FieldSet {
return slice(
Posts.columns +
Users.id +
Users.displayName +
Users.username +
Users.avatar +
Users.verified +
Posts.likesCount +
Posts.repliesCount +
Posts.repostsCount +
Posts.likesIn +
additionalFields
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,32 @@ package me.nathanfallet.extopy.plugins
import io.ktor.server.application.*
import me.nathanfallet.extopy.controllers.auth.AuthRouter
import me.nathanfallet.extopy.controllers.notifications.NotificationsRouter
import me.nathanfallet.extopy.controllers.posts.PostsController
import me.nathanfallet.extopy.controllers.posts.PostsRouter
import me.nathanfallet.extopy.controllers.users.UsersController
import me.nathanfallet.extopy.controllers.users.UsersRouter
import me.nathanfallet.extopy.database.Database
import me.nathanfallet.extopy.database.application.DatabaseCodesInEmailsRepository
import me.nathanfallet.extopy.database.posts.DatabasePostsRepository
import me.nathanfallet.extopy.database.users.DatabaseUsersRepository
import me.nathanfallet.extopy.models.auth.LoginPayload
import me.nathanfallet.extopy.models.auth.RegisterCodePayload
import me.nathanfallet.extopy.models.auth.RegisterPayload
import me.nathanfallet.extopy.models.posts.Post
import me.nathanfallet.extopy.models.posts.PostPayload
import me.nathanfallet.extopy.models.users.CreateUserPayload
import me.nathanfallet.extopy.models.users.UpdateUserPayload
import me.nathanfallet.extopy.models.users.User
import me.nathanfallet.extopy.repositories.application.ICodesInEmailsRepository
import me.nathanfallet.extopy.repositories.posts.IPostsRepository
import me.nathanfallet.extopy.repositories.users.IUsersRepository
import me.nathanfallet.extopy.services.emails.EmailsService
import me.nathanfallet.extopy.services.emails.IEmailsService
import me.nathanfallet.extopy.usecases.application.SendEmailUseCase
import me.nathanfallet.extopy.usecases.auth.*
import me.nathanfallet.extopy.usecases.posts.CreatePostUseCase
import me.nathanfallet.extopy.usecases.posts.DeletePostUseCase
import me.nathanfallet.extopy.usecases.posts.UpdatePostUseCase
import me.nathanfallet.extopy.usecases.users.CreateUserUseCase
import me.nathanfallet.extopy.usecases.users.GetUserForCallUseCase
import me.nathanfallet.extopy.usecases.users.UpdateUserUseCase
Expand All @@ -37,6 +45,8 @@ import me.nathanfallet.ktorx.usecases.users.RequireUserForCallUseCase
import me.nathanfallet.usecases.emails.ISendEmailUseCase
import me.nathanfallet.usecases.localization.ITranslateUseCase
import me.nathanfallet.usecases.models.create.ICreateModelSuspendUseCase
import me.nathanfallet.usecases.models.create.context.ICreateModelWithContextSuspendUseCase
import me.nathanfallet.usecases.models.delete.IDeleteModelSuspendUseCase
import me.nathanfallet.usecases.models.get.context.GetModelWithContextFromRepositorySuspendUseCase
import me.nathanfallet.usecases.models.get.context.IGetModelWithContextSuspendUseCase
import me.nathanfallet.usecases.models.update.IUpdateModelSuspendUseCase
Expand Down Expand Up @@ -69,6 +79,7 @@ fun Application.configureKoin() {
val repositoryModule = module {
single<ICodesInEmailsRepository> { DatabaseCodesInEmailsRepository(get()) }
single<IUsersRepository> { DatabaseUsersRepository(get()) }
single<IPostsRepository> { DatabasePostsRepository(get()) }
}
val useCaseModule = module {
// Application
Expand Down Expand Up @@ -109,6 +120,20 @@ fun Application.configureKoin() {
single<IUpdateModelSuspendUseCase<User, String, UpdateUserPayload>>(named<User>()) {
UpdateUserUseCase(get(), get())
}

// Posts
single<IGetModelWithContextSuspendUseCase<Post, String>>(named<Post>()) {
GetModelWithContextFromRepositorySuspendUseCase(get<IPostsRepository>())
}
single<ICreateModelWithContextSuspendUseCase<Post, PostPayload>>(named<Post>()) {
CreatePostUseCase(get())
}
single<IUpdateModelSuspendUseCase<Post, String, PostPayload>>(named<Post>()) {
UpdatePostUseCase(get())
}
single<IDeleteModelSuspendUseCase<Post, String>>(named<Post>()) {
DeletePostUseCase(get())
}
}
val controllerModule = module {
// Auth
Expand All @@ -132,12 +157,23 @@ fun Application.configureKoin() {
get(named<User>())
)
}

// Posts
single<IModelController<Post, String, PostPayload, PostPayload>>(named<Post>()) {
PostsController(
get(),
get(named<Post>()),
get(named<Post>()),
get(named<Post>()),
get(named<Post>())
)
}
}
val routerModule = module {
single { AuthRouter(get(), get()) }
single { UsersRouter(get(named<User>())) }
single { PostsRouter(get(named<Post>())) }
single { NotificationsRouter() }
single { PostsRouter() }
}

modules(
Expand Down
Loading

0 comments on commit d169b24

Please sign in to comment.