Skip to content

Commit

Permalink
feat: search posts and users (server side)
Browse files Browse the repository at this point in the history
  • Loading branch information
nathanfallet committed Mar 5, 2024
1 parent 5e17bcc commit bae474d
Show file tree
Hide file tree
Showing 10 changed files with 167 additions and 15 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,16 @@ import me.nathanfallet.ktorx.models.annotations.*

interface IPostsController : IModelController<Post, String, PostPayload, PostPayload> {

@APIMapping
@ListModelPath
@DocumentedError(401, "auth_invalid_credentials")
suspend fun list(
call: ApplicationCall,
@QueryParameter limit: Long?,
@QueryParameter offset: Long?,
@QueryParameter search: String?,
): List<Post>

@APIMapping
@GetModelPath
@DocumentedError(401, "auth_invalid_credentials")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package me.nathanfallet.extopy.controllers.posts

import io.ktor.http.*
import io.ktor.server.application.*
import me.nathanfallet.extopy.models.application.SearchOptions
import me.nathanfallet.extopy.models.posts.Post
import me.nathanfallet.extopy.models.posts.PostPayload
import me.nathanfallet.extopy.models.users.User
Expand All @@ -12,18 +13,32 @@ import me.nathanfallet.ktorx.usecases.users.IRequireUserForCallUseCase
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.list.slice.context.IListSliceModelWithContextSuspendUseCase
import me.nathanfallet.usecases.models.update.IUpdateModelSuspendUseCase
import me.nathanfallet.usecases.pagination.Pagination

class PostsController(
private val requireUserForCallUseCase: IRequireUserForCallUseCase,
private val createPostUseCase: ICreateModelWithContextSuspendUseCase<Post, PostPayload>,
private val listPostsUseCase: IListSliceModelWithContextSuspendUseCase<Post>,
private val getPostUseCase: IGetModelWithContextSuspendUseCase<Post, String>,
private val updatePostUseCase: IUpdateModelSuspendUseCase<Post, String, PostPayload>,
private val deletePostUseCase: IDeleteModelSuspendUseCase<Post, String>,
private val getPostRepliesUseCase: IGetPostRepliesUseCase,
) : IPostsController {

override suspend fun list(call: ApplicationCall, limit: Long?, offset: Long?, search: String?): List<Post> {
val user = requireUserForCallUseCase(call) as User
return listPostsUseCase(
Pagination(
limit ?: 25,
offset ?: 0,
search?.let { SearchOptions(it) }
),
UserContext(user.id)
)
}

override suspend fun create(call: ApplicationCall, payload: PostPayload): Post {
val user = requireUserForCallUseCase(call) as User
return createPostUseCase(payload, UserContext(user.id)) ?: throw ControllerException(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,16 @@ import me.nathanfallet.ktorx.models.annotations.*

interface IUsersController : IModelController<User, String, CreateUserPayload, UpdateUserPayload> {

@APIMapping
@ListModelPath
@DocumentedError(401, "auth_invalid_credentials")
suspend fun list(
call: ApplicationCall,
@QueryParameter limit: Long?,
@QueryParameter offset: Long?,
@QueryParameter search: String?,
): List<User>

@APIMapping
@GetModelPath
@DocumentedError(401, "auth_invalid_credentials")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package me.nathanfallet.extopy.controllers.users

import io.ktor.http.*
import io.ktor.server.application.*
import me.nathanfallet.extopy.models.application.SearchOptions
import me.nathanfallet.extopy.models.posts.Post
import me.nathanfallet.extopy.models.users.UpdateUserPayload
import me.nathanfallet.extopy.models.users.User
Expand All @@ -10,16 +11,30 @@ import me.nathanfallet.extopy.usecases.users.IGetUserPostsUseCase
import me.nathanfallet.ktorx.models.exceptions.ControllerException
import me.nathanfallet.ktorx.usecases.users.IRequireUserForCallUseCase
import me.nathanfallet.usecases.models.get.context.IGetModelWithContextSuspendUseCase
import me.nathanfallet.usecases.models.list.slice.context.IListSliceModelWithContextSuspendUseCase
import me.nathanfallet.usecases.models.update.IUpdateModelSuspendUseCase
import me.nathanfallet.usecases.pagination.Pagination

class UsersController(
private val requireUserForCallUseCase: IRequireUserForCallUseCase,
private val listUsersUseCase: IListSliceModelWithContextSuspendUseCase<User>,
private val getUserUseCase: IGetModelWithContextSuspendUseCase<User, String>,
private val updateUserUseCase: IUpdateModelSuspendUseCase<User, String, UpdateUserPayload>,
private val getUserPostsUseCase: IGetUserPostsUseCase,
) : IUsersController {

override suspend fun list(call: ApplicationCall, limit: Long?, offset: Long?, search: String?): List<User> {
val user = requireUserForCallUseCase(call) as User
return listUsersUseCase(
Pagination(
limit ?: 25,
offset ?: 0,
search?.let { SearchOptions(it) }
),
UserContext(user.id)
)
}

override suspend fun get(call: ApplicationCall, id: String): User {
val user = requireUserForCallUseCase(call) as User
return getUserUseCase(id, UserContext(user.id)) ?: throw ControllerException(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@ package me.nathanfallet.extopy.database.posts
import kotlinx.datetime.Clock
import me.nathanfallet.extopy.database.users.FollowersInUsers
import me.nathanfallet.extopy.database.users.Users
import me.nathanfallet.extopy.models.application.SearchOptions
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.surexposed.database.IDatabase
import me.nathanfallet.usecases.context.IContext
import me.nathanfallet.usecases.pagination.IPaginationOptions
import me.nathanfallet.usecases.pagination.Pagination
import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
Expand All @@ -23,6 +25,18 @@ class PostsDatabaseRepository(
}
}

override suspend fun list(pagination: Pagination, context: IContext?): List<Post> {
if (context !is UserContext) return emptyList()
return database.suspendedTransaction {
customJoin(context.userId)
.groupBy(Posts.id)
.andWhere(pagination.options)
.orderBy(Posts.published to SortOrder.DESC)
.limit(pagination.limit.toInt(), pagination.offset)
.map { Posts.toPost(it, Users.toUser(it)) }
}
}

override suspend fun listDefault(pagination: Pagination, context: UserContext): List<Post> =
database.suspendedTransaction {
customJoinColumnSet(context.userId)
Expand Down Expand Up @@ -149,4 +163,20 @@ class PostsDatabaseRepository(
additionalFields
)

private fun Query.andWhere(options: IPaginationOptions?): Query = when (options) {
is SearchOptions -> {
val likeString = options.search
.replace("%", "\\%")
.replace("_", "\\_")
andWhere {
likeString.split(" ")
.map { Posts.body like "%$it%" }
.fold(Op.FALSE as Op<Boolean>) { a, b -> a or b }
}

}

else -> this
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,16 @@ package me.nathanfallet.extopy.database.users

import kotlinx.datetime.Clock
import me.nathanfallet.extopy.database.posts.Posts
import me.nathanfallet.extopy.models.application.SearchOptions
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.models.users.UserContext
import me.nathanfallet.extopy.repositories.users.IUsersRepository
import me.nathanfallet.surexposed.database.IDatabase
import me.nathanfallet.usecases.context.IContext
import me.nathanfallet.usecases.pagination.IPaginationOptions
import me.nathanfallet.usecases.pagination.Pagination
import org.jetbrains.exposed.sql.*

class UsersDatabaseRepository(
Expand All @@ -21,6 +24,18 @@ class UsersDatabaseRepository(
}
}

override suspend fun list(pagination: Pagination, context: IContext?): List<User> {
if (context !is UserContext) return emptyList()
return database.suspendedTransaction {
customJoin(context.userId)
.groupBy(Users.id)
.andWhere(pagination.options)
.orderBy(Users.joinDate to SortOrder.DESC)
.limit(pagination.limit.toInt(), pagination.offset)
.map(Users::toUser)
}
}

override suspend fun get(id: String, context: IContext?): User? {
if (context !is UserContext) return null
return database.suspendedTransaction {
Expand Down Expand Up @@ -131,4 +146,22 @@ class UsersDatabaseRepository(
Users.followingIn
)

private fun Query.andWhere(options: IPaginationOptions?): Query = when (options) {
is SearchOptions -> {
val likeString = options.search
.replace("%", "\\%")
.replace("_", "\\_")
andWhere {
likeString.split(" ").map {
Users.username like "%$it%" or
(Users.displayName like "%$it%") or
(Users.biography like "%$it%")
}.fold(Op.FALSE as Op<Boolean>) { a, b -> a or b }
}

}

else -> this
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,8 @@ import me.nathanfallet.usecases.models.get.context.GetModelWithContextFromReposi
import me.nathanfallet.usecases.models.get.context.IGetModelWithContextSuspendUseCase
import me.nathanfallet.usecases.models.list.slice.IListSliceChildModelSuspendUseCase
import me.nathanfallet.usecases.models.list.slice.ListSliceChildModelFromRepositorySuspendUseCase
import me.nathanfallet.usecases.models.list.slice.context.IListSliceModelWithContextSuspendUseCase
import me.nathanfallet.usecases.models.list.slice.context.ListSliceModelWithContextFromRepositorySuspendUseCase
import me.nathanfallet.usecases.models.repositories.IChildModelSuspendRepository
import me.nathanfallet.usecases.models.repositories.IModelSuspendRepository
import me.nathanfallet.usecases.models.update.IUpdateModelSuspendUseCase
Expand Down Expand Up @@ -168,6 +170,9 @@ fun Application.configureKoin() {
// Users
single<IRequireUserForCallUseCase> { RequireUserForCallUseCase(get()) }
single<IGetUserForCallUseCase> { GetUserForCallUseCase(get(), get(), get(named<User>())) }
single<IListSliceModelWithContextSuspendUseCase<User>>(named<User>()) {
ListSliceModelWithContextFromRepositorySuspendUseCase(get<IUsersRepository>())
}
single<IGetModelWithContextSuspendUseCase<User, String>>(named<User>()) {
GetModelWithContextFromRepositorySuspendUseCase(get<IUsersRepository>())
}
Expand All @@ -190,6 +195,9 @@ fun Application.configureKoin() {
single<IListFollowingInUserUseCase> { ListFollowingInUserUseCase(get()) }

// Posts
single<IListSliceModelWithContextSuspendUseCase<Post>>(named<Post>()) {
ListSliceModelWithContextFromRepositorySuspendUseCase(get<IPostsRepository>())
}
single<IGetModelWithContextSuspendUseCase<Post, String>>(named<Post>()) {
GetModelWithContextFromRepositorySuspendUseCase(get<IPostsRepository>())
}
Expand Down Expand Up @@ -248,6 +256,7 @@ fun Application.configureKoin() {
get(),
get(named<User>()),
get(named<User>()),
get(named<User>()),
get()
)
}
Expand All @@ -269,6 +278,7 @@ fun Application.configureKoin() {
get(named<Post>()),
get(named<Post>()),
get(named<Post>()),
get(named<Post>()),
get()
)
}
Expand Down

0 comments on commit bae474d

Please sign in to comment.