1 Commits

Author SHA1 Message Date
hubert 32337ec308 Use mapstruct 2020-10-21 22:02:34 +02:00
11 changed files with 77 additions and 90 deletions
+24 -21
View File
@@ -1,6 +1,6 @@
package be.simplenotes.app.api package be.simplenotes.app.api
import be.simplenotes.app.extensions.auto import be.simplenotes.app.extensions.json
import be.simplenotes.app.utils.parseSearchTerms import be.simplenotes.app.utils.parseSearchTerms
import be.simplenotes.domain.model.PersistedNote import be.simplenotes.domain.model.PersistedNote
import be.simplenotes.domain.model.PersistedNoteMetadata import be.simplenotes.domain.model.PersistedNoteMetadata
@@ -8,39 +8,48 @@ import be.simplenotes.domain.security.JwtPayload
import be.simplenotes.domain.usecases.NoteService import be.simplenotes.domain.usecases.NoteService
import kotlinx.serialization.Contextual import kotlinx.serialization.Contextual
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.builtins.ListSerializer
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import org.http4k.core.Request import org.http4k.core.Request
import org.http4k.core.Response import org.http4k.core.Response
import org.http4k.core.Status.Companion.BAD_REQUEST import org.http4k.core.Status.Companion.BAD_REQUEST
import org.http4k.core.Status.Companion.NOT_FOUND import org.http4k.core.Status.Companion.NOT_FOUND
import org.http4k.core.Status.Companion.OK import org.http4k.core.Status.Companion.OK
import org.http4k.lens.Path import org.http4k.routing.path
import org.http4k.lens.uuid
import java.util.* import java.util.*
class ApiNoteController(private val noteService: NoteService, private val json: Json) { class ApiNoteController(private val noteService: NoteService, private val json: Json) {
fun createNote(request: Request, jwtPayload: JwtPayload): Response { fun createNote(request: Request, jwtPayload: JwtPayload): Response {
val content = noteContentLens(request) val content = json.decodeFromString(NoteContent.serializer(), request.bodyString()).content
return noteService.create(jwtPayload.userId, content).fold( return noteService.create(jwtPayload.userId, content).fold(
{ Response(BAD_REQUEST) }, {
{ uuidContentLens(UuidContent(it.uuid), Response(OK)) } Response(BAD_REQUEST)
},
{
Response(OK).json(json.encodeToString(UuidContent.serializer(), UuidContent(it.uuid)))
}
) )
} }
fun notes(request: Request, jwtPayload: JwtPayload): Response { fun notes(request: Request, jwtPayload: JwtPayload): Response {
val notes = noteService.paginatedNotes(jwtPayload.userId, page = 1).notes val notes = noteService.paginatedNotes(jwtPayload.userId, page = 1).notes
return persistedNotesMetadataLens(notes, Response(OK)) val json = json.encodeToString(ListSerializer(PersistedNoteMetadata.serializer()), notes)
return Response(OK).json(json)
} }
fun note(request: Request, jwtPayload: JwtPayload): Response = fun note(request: Request, jwtPayload: JwtPayload): Response {
noteService.find(jwtPayload.userId, uuidLens(request)) val uuid = request.path("uuid")!!
?.let { persistedNoteLens(it, Response(OK)) }
return noteService.find(jwtPayload.userId, UUID.fromString(uuid))
?.let { Response(OK).json(json.encodeToString(PersistedNote.serializer(), it)) }
?: Response(NOT_FOUND) ?: Response(NOT_FOUND)
}
fun update(request: Request, jwtPayload: JwtPayload): Response { fun update(request: Request, jwtPayload: JwtPayload): Response {
val content = noteContentLens(request) val uuid = UUID.fromString(request.path("uuid")!!)
return noteService.update(jwtPayload.userId, uuidLens(request), content).fold({ val content = json.decodeFromString(NoteContent.serializer(), request.bodyString()).content
return noteService.update(jwtPayload.userId, uuid, content).fold({
Response(BAD_REQUEST) Response(BAD_REQUEST)
}, { }, {
if (it == null) Response(NOT_FOUND) if (it == null) Response(NOT_FOUND)
@@ -49,19 +58,13 @@ class ApiNoteController(private val noteService: NoteService, private val json:
} }
fun search(request: Request, jwtPayload: JwtPayload): Response { fun search(request: Request, jwtPayload: JwtPayload): Response {
val query = searchContentLens(request) val query = json.decodeFromString(SearchContent.serializer(), request.bodyString()).query
val terms = parseSearchTerms(query) val terms = parseSearchTerms(query)
val notes = noteService.search(jwtPayload.userId, terms) val notes = noteService.search(jwtPayload.userId, terms)
return persistedNotesMetadataLens(notes, Response(OK)) val json = json.encodeToString(ListSerializer(PersistedNoteMetadata.serializer()), notes)
return Response(OK).json(json)
} }
private val uuidContentLens = json.auto<UuidContent>().toLens()
private val noteContentLens = json.auto<NoteContent>().map { it.content }.toLens()
private val searchContentLens = json.auto<SearchContent>().map { it.query }.toLens()
private val persistedNotesMetadataLens = json.auto<List<PersistedNoteMetadata>>().toLens()
private val persistedNoteLens = json.auto<PersistedNote>().toLens()
private val uuidLens = Path.uuid().of("uuid")
} }
@Serializable @Serializable
+11 -11
View File
@@ -1,25 +1,25 @@
package be.simplenotes.app.api package be.simplenotes.app.api
import be.simplenotes.app.extensions.auto import be.simplenotes.app.extensions.json
import be.simplenotes.domain.usecases.UserService import be.simplenotes.domain.usecases.UserService
import be.simplenotes.domain.usecases.users.login.LoginForm import be.simplenotes.domain.usecases.users.login.LoginForm
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import org.http4k.core.Request import org.http4k.core.Request
import org.http4k.core.Response import org.http4k.core.Response
import org.http4k.core.Status.Companion.BAD_REQUEST import org.http4k.core.Status
import org.http4k.core.Status.Companion.OK
class ApiUserController(private val userService: UserService, private val json: Json) { class ApiUserController(private val userService: UserService, private val json: Json) {
private val tokenLens = json.auto<Token>().toLens()
private val loginFormLens = json.auto<LoginForm>().toLens()
fun login(request: Request) = userService fun login(request: Request): Response {
.login(loginFormLens(request)) val form = json.decodeFromString(LoginForm.serializer(), request.bodyString())
.fold( val result = userService.login(form)
{ Response(BAD_REQUEST) }, return result.fold({
{ tokenLens(Token(it), Response(OK)) } Response(Status.BAD_REQUEST)
) }, {
Response(Status.OK).json(json.encodeToString(Token.serializer(), Token(it)))
})
}
} }
@Serializable @Serializable
@@ -1,35 +1,17 @@
package be.simplenotes.app.extensions package be.simplenotes.app.extensions
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import org.http4k.asString
import org.http4k.core.Body
import org.http4k.core.ContentType
import org.http4k.core.Request import org.http4k.core.Request
import org.http4k.core.Response import org.http4k.core.Response
import org.http4k.core.Status.Companion.FOUND import org.http4k.core.Status.Companion.FOUND
import org.http4k.core.Status.Companion.MOVED_PERMANENTLY import org.http4k.core.Status.Companion.MOVED_PERMANENTLY
import org.http4k.lens.*
fun Response.html(html: String) = body(html) fun Response.html(html: String) = body(html)
.header("Content-Type", "text/html; charset=utf-8") .header("Content-Type", "text/html; charset=utf-8")
.header("Cache-Control", "no-cache") .header("Cache-Control", "no-cache")
fun Response.json(json: String) = body(json).header("Content-Type", "application/json")
fun Response.Companion.redirect(url: String, permanent: Boolean = false) = fun Response.Companion.redirect(url: String, permanent: Boolean = false) =
Response(if (permanent) MOVED_PERMANENTLY else FOUND).header("Location", url) Response(if (permanent) MOVED_PERMANENTLY else FOUND).header("Location", url)
fun Request.isSecure() = header("X-Forwarded-Proto")?.contains("https") ?: false fun Request.isSecure() = header("X-Forwarded-Proto")?.contains("https") ?: false
val bodyLens = httpBodyRoot(
listOf(Meta(true, "body", ParamMeta.ObjectParam, "body")),
ContentType.APPLICATION_JSON.withNoDirectives(), ContentNegotiation.StrictNoDirective
).map(
{ it.payload.asString() },
{ Body(it) }
)
inline fun <reified T> Json.auto(): BiDiBodyLensSpec<T> = bodyLens.map(
{ decodeFromString(it) },
{ encodeToString(it) }
)
+1 -1
View File
@@ -13,7 +13,7 @@ class BaseView(staticFileResolver: StaticFileResolver) : View(staticFileResolver
) { ) {
section("text-center my-2 p-2") { section("text-center my-2 p-2") {
h1("text-5xl casual") { h1("text-5xl casual") {
span("text-teal-300") { +"SimpleNotes " } span("text-teal-300") { +"Simplenotes " }
+"- access your notes anywhere" +"- access your notes anywhere"
} }
} }
+23 -20
View File
@@ -143,6 +143,7 @@ class NoteView(staticFileResolver: StaticFileResolver) : View(staticFileResolver
} }
if (!shared) { if (!shared) {
noteActionForm(note) noteActionForm(note)
publicPrivateForm(note)
if (note.public) { if (note.public) {
p("my-4") { p("my-4") {
@@ -165,29 +166,12 @@ class NoteView(staticFileResolver: StaticFileResolver) : View(staticFileResolver
} }
private fun DIV.noteActionForm(note: PersistedNote) { private fun DIV.noteActionForm(note: PersistedNote) {
form(method = FormMethod.post, classes = "inline flex space-x-2 justify-end mb-4") { span("flex space-x-2 justify-end mb-4") {
a( a(
href = "/notes/${note.uuid}/edit", href = "/notes/${note.uuid}/edit",
classes = "btn btn-green" classes = "btn btn-teal"
) { +"Edit" } ) { +"Edit" }
span { form(method = FormMethod.post, classes = "inline") {
button(
type = ButtonType.submit,
name = if (note.public) "private" else "public",
classes = "font-semibold border-b-4 ${if (note.public) "border-teal-200" else "border-green-500"}" +
" p-2 rounded-l bg-teal-200 text-gray-800"
) {
+"Private"
}
button(
type = ButtonType.submit,
name = if (note.public) "private" else "public",
classes = "font-semibold border-b-4 ${if (!note.public) "border-teal-200" else "border-green-500"}" +
" p-2 rounded-r bg-teal-200 text-gray-800"
) {
+"Public"
}
}
button( button(
type = ButtonType.submit, type = ButtonType.submit,
name = "delete", name = "delete",
@@ -195,4 +179,23 @@ class NoteView(staticFileResolver: StaticFileResolver) : View(staticFileResolver
) { +"Delete" } ) { +"Delete" }
} }
} }
}
private fun DIV.publicPrivateForm(note: PersistedNote) {
span("flex space-x-2 justify-end mb-4") {
form(method = FormMethod.post, classes = "ml-auto ") {
button(
type = ButtonType.submit,
name = if (note.public) "private" else "public",
classes = "btn btn-teal"
) {
if (note.public)
+"This note is public, do you want to make it private ?"
else
+"This note is private, do you want to make it public ?"
}
}
}
}
} }
+5 -6
View File
@@ -26,23 +26,22 @@ class SettingView(staticFileResolver: StaticFileResolver) : View(staticFileResol
} }
} }
section("m-4 p-2 bg-gray-800 rounded flex flex-wrap justify-around items-end") { section("m-4 p-4 bg-gray-800 rounded flex justify-around") {
form(classes = "m-2", method = FormMethod.post, action = "/export") { form(method = FormMethod.post, action = "/export") {
button(name = "display", button(name = "display",
classes = "inline btn btn-teal block", classes = "inline btn btn-teal block",
type = submit) { +"Display my data" } type = submit) { +"Display my data" }
} }
form(classes = "m-2", method = FormMethod.post, action = "/export") { form(method = FormMethod.post, action = "/export") {
div {
listOf("json", "zip").forEach { format -> listOf("json", "zip").forEach { format ->
div {
radioInput(name = "format") { radioInput(name = "format") {
id = format id = format
attributes["value"] = format attributes["value"] = format
if (format == "json") attributes["checked"] = "" if(format == "json") attributes["checked"] = ""
else attributes["class"] = "ml-4"
} }
label(classes = "ml-2") { label(classes = "ml-2") {
attributes["for"] = format attributes["for"] = format
@@ -25,8 +25,8 @@ fun FlowContent.deletedNoteTable(notes: List<PersistedNoteMetadata>) = div("over
td("text-center") { +updatedAt.toTimeAgo() } td("text-center") { +updatedAt.toTimeAgo() }
td { tags(tags) } td { tags(tags) }
td("text-center") { td("text-center") {
form(method = post, action = "/notes/deleted/$uuid") { form(classes = "inline", method = post, action = "/notes/deleted/$uuid") {
button(classes = "btn btn-red mb-2", type = submit, name = "delete") { button(classes = "btn btn-red", type = submit, name = "delete") {
+"Delete permanently" +"Delete permanently"
} }
button(classes = "ml-2 btn btn-green", type = submit, name = "restore") { button(classes = "ml-2 btn btn-green", type = submit, name = "restore") {
@@ -15,6 +15,7 @@ import me.liuwj.ktorm.database.Database
import org.koin.dsl.bind import org.koin.dsl.bind
import org.koin.dsl.module import org.koin.dsl.module
import org.koin.dsl.onClose import org.koin.dsl.onClose
import org.mapstruct.factory.Mappers
import javax.sql.DataSource import javax.sql.DataSource
private fun hikariDataSource(conf: DataSourceConfig): HikariDataSource { private fun hikariDataSource(conf: DataSourceConfig): HikariDataSource {
@@ -8,10 +8,9 @@ import org.mapstruct.Mapper
@Mapper(uses = [UserEntityFactory::class]) @Mapper(uses = [UserEntityFactory::class])
internal interface UserConverter { internal interface UserConverter {
fun toUser(userEntity: UserEntity): User fun convertToUser(userEntity: UserEntity): User
fun toPersistedUser(userEntity: UserEntity): PersistedUser fun convertToPersistedUser(userEntity: UserEntity): PersistedUser
fun toEntity(user: User): UserEntity fun convertToEntity(user: User): UserEntity
fun toEntity(user: PersistedUser): UserEntity
} }
internal class UserEntityFactory : Entity.Factory<UserEntity>() internal class UserEntityFactory : Entity.Factory<UserEntity>()
@@ -24,10 +24,10 @@ internal class UserRepositoryImpl(private val db: Database, private val converte
} }
override fun find(username: String) = db.users.find { it.username eq username } override fun find(username: String) = db.users.find { it.username eq username }
?.let { converter.toPersistedUser(it) } ?.let { converter.convertToPersistedUser(it) }
override fun find(id: Int) = db.users.find { it.id eq id }?.let { override fun find(id: Int) = db.users.find { it.id eq id }?.let {
converter.toPersistedUser(it) converter.convertToPersistedUser(it)
} }
override fun exists(username: String) = db.users.any { it.username eq username } override fun exists(username: String) = db.users.any { it.username eq username }
@@ -18,7 +18,7 @@ internal class UserConverterTest {
}.apply { }.apply {
this["id"] = 2 this["id"] = 2
} }
val user = converter.toUser(entity) val user = converter.convertToUser(entity)
assertThat(user).isEqualTo(User("test", "test2")) assertThat(user).isEqualTo(User("test", "test2"))
} }
@@ -31,7 +31,7 @@ internal class UserConverterTest {
}.apply { }.apply {
this["id"] = 2 this["id"] = 2
} }
val user = converter.toPersistedUser(entity) val user = converter.convertToPersistedUser(entity)
assertThat(user).isEqualTo(PersistedUser("test", "test2", 2)) assertThat(user).isEqualTo(PersistedUser("test", "test2", 2))
} }
@@ -39,7 +39,7 @@ internal class UserConverterTest {
fun `convert User to UserEntity`() { fun `convert User to UserEntity`() {
val converter = Mappers.getMapper(UserConverter::class.java) val converter = Mappers.getMapper(UserConverter::class.java)
val user = User("test", "test2") val user = User("test", "test2")
val entity = converter.toEntity(user) val entity = converter.convertToEntity(user)
assertThat(entity) assertThat(entity)
.hasFieldOrPropertyWithValue("username", "test") .hasFieldOrPropertyWithValue("username", "test")