Lots of things

This commit is contained in:
Hubert Van De Walle 2020-06-22 17:34:45 +02:00
parent f134a83604
commit 3306b16f91
21 changed files with 482 additions and 344 deletions

View File

@ -5,29 +5,21 @@ create table Users
password varchar(255) not null,
constraint username unique (username)
);
) character set 'utf8mb4'
collate 'utf8mb4_general_ci';
create table Notes
(
uuid binary(16) not null primary key,
title varchar(50) not null,
content text not null,
user_id int not null,
updated_at datetime null,
constraint Notes_fk_user foreign key (user_id) references Users (id) on delete cascade
);
create table Chapters
(
id int auto_increment primary key,
number int not null,
title varchar(50) not null,
content text not null,
note_uuid binary(16) not null,
constraint Chapters_fk_note foreign key (note_uuid) references Notes (uuid) on delete cascade
);
create index note_uuid on Chapters (note_uuid);
) character set 'utf8mb4'
collate 'utf8mb4_general_ci';
create index user_id on Notes (user_id);
@ -37,6 +29,7 @@ create table Tags
name varchar(50) not null,
note_uuid binary(16) not null,
constraint Tags_fk_note foreign key (note_uuid) references Notes (uuid) on delete cascade
);
) character set 'utf8mb4'
collate 'utf8mb4_general_ci';
create index note_uuid on Tags (note_uuid);

View File

@ -1,24 +1,17 @@
package be.vandewalleh
import be.vandewalleh.features.Config
import be.vandewalleh.features.configurationModule
import be.vandewalleh.features.loadFeatures
import be.vandewalleh.migrations.Migration
import be.vandewalleh.routing.registerRoutes
import be.vandewalleh.services.serviceModule
import io.ktor.application.*
import io.ktor.routing.*
import io.ktor.server.engine.*
import io.ktor.server.netty.*
import me.liuwj.ktorm.database.*
import org.kodein.di.Kodein
import org.kodein.di.description
import org.kodein.di.generic.bind
import org.kodein.di.generic.instance
import org.kodein.di.generic.singleton
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import javax.sql.DataSource
fun main() {

View File

@ -1,13 +0,0 @@
package be.vandewalleh.entities
import me.liuwj.ktorm.entity.*
interface Chapter : Entity<Chapter> {
companion object : Entity.Factory<Chapter>()
val id: Int
var number: Int
var title: String
var content: String
var note: Note
}

View File

@ -1,5 +1,6 @@
package be.vandewalleh.entities
import com.fasterxml.jackson.annotation.JsonIgnore
import me.liuwj.ktorm.entity.*
import java.time.LocalDateTime
import java.util.*
@ -9,6 +10,12 @@ interface Note : Entity<Note> {
var uuid: UUID
var title: String
var user: User
var content: String
var updatedAt: LocalDateTime
}
@get:JsonIgnore
var user: User
// Not part of the Notes table
var tags: List<String>
}

View File

@ -1,12 +1,9 @@
package be.vandewalleh.extensions
import be.vandewalleh.auth.UserDbIdPrincipal
import be.vandewalleh.services.FullNoteCreateDTO
import be.vandewalleh.services.FullNotePatchDTO
import io.ktor.application.*
import io.ktor.auth.*
import io.ktor.http.*
import io.ktor.request.*
import io.ktor.response.*
suspend fun ApplicationCall.respondStatus(status: HttpStatusCode) {
@ -17,9 +14,3 @@ suspend fun ApplicationCall.respondStatus(status: HttpStatusCode) {
* @return the userId for the currently authenticated user
*/
fun ApplicationCall.userId() = principal<UserDbIdPrincipal>()!!.id
class NoteCreate(val title: String, val tags: List<String>)
suspend fun ApplicationCall.receiveNoteCreate(): FullNoteCreateDTO = receive()
suspend fun ApplicationCall.receiveNotePatch(): FullNotePatchDTO = receive()

View File

@ -3,10 +3,6 @@ package be.vandewalleh.extensions
import io.ktor.http.*
import java.util.*
fun Parameters.noteTitle(): String {
return this["noteTitle"]!!
}
fun Parameters.noteUuid(): UUID {
return UUID.fromString(this["noteUuid"])
}
}

View File

@ -1,5 +1,10 @@
package be.vandewalleh.routing
import be.vandewalleh.routing.notes.notes
import be.vandewalleh.routing.notes.tags
import be.vandewalleh.routing.notes.title
import be.vandewalleh.routing.user.auth
import be.vandewalleh.routing.user.user
import io.ktor.routing.*
import org.kodein.di.Kodein
@ -9,4 +14,4 @@ fun Routing.registerRoutes(kodein: Kodein) {
notes(kodein)
title(kodein)
tags(kodein)
}
}

View File

@ -1,55 +0,0 @@
package be.vandewalleh.routing
import be.vandewalleh.extensions.noteUuid
import be.vandewalleh.extensions.receiveNotePatch
import be.vandewalleh.extensions.respondStatus
import be.vandewalleh.extensions.userId
import be.vandewalleh.services.NotesService
import io.ktor.application.call
import io.ktor.auth.authenticate
import io.ktor.http.HttpStatusCode
import io.ktor.response.respond
import io.ktor.routing.*
import org.kodein.di.Kodein
import org.kodein.di.generic.instance
fun Routing.title(kodein: Kodein) {
val notesService by kodein.instance<NotesService>()
authenticate {
route("/notes/{noteUuid}") {
get {
val userId = call.userId()
val noteUuid = call.parameters.noteUuid()
val response =
notesService.getNote(userId, noteUuid) ?: return@get call.respondStatus(HttpStatusCode.NotFound)
call.respond(response)
}
patch {
val userId = call.userId()
val noteUuid = call.parameters.noteUuid()
val exists = notesService.noteExists(userId, noteUuid)
if (!exists) return@patch call.respondStatus(HttpStatusCode.NotFound)
val notePatch = call.receiveNotePatch().copy(uuid = noteUuid)
notesService.updateNote(notePatch)
call.respondStatus(HttpStatusCode.OK)
}
delete {
val userId = call.userId()
val noteUuid = call.parameters.noteUuid()
val exists = notesService.noteExists(userId, noteUuid)
if (!exists) return@delete call.respondStatus(HttpStatusCode.NotFound)
notesService.deleteNote(noteUuid)
call.respondStatus(HttpStatusCode.OK)
}
}
}
}

View File

@ -0,0 +1,53 @@
package be.vandewalleh.routing.notes
import be.vandewalleh.entities.Note
import be.vandewalleh.extensions.noteUuid
import be.vandewalleh.extensions.respondStatus
import be.vandewalleh.extensions.userId
import be.vandewalleh.services.NoteService
import io.ktor.application.call
import io.ktor.auth.authenticate
import io.ktor.http.HttpStatusCode
import io.ktor.request.*
import io.ktor.response.respond
import io.ktor.routing.*
import org.kodein.di.Kodein
import org.kodein.di.generic.instance
fun Routing.title(kodein: Kodein) {
val noteService by kodein.instance<NoteService>()
authenticate {
route("/notes/{noteUuid}") {
get {
val userId = call.userId()
val noteUuid = call.parameters.noteUuid()
val response = noteService.find(userId, noteUuid)
?: return@get call.respondStatus(HttpStatusCode.NotFound)
call.respond(response)
}
put {
val userId = call.userId()
val noteUuid = call.parameters.noteUuid()
val note = call.receive<Note>()
note.uuid = noteUuid
if (noteService.updateNote(userId, note))
call.respondStatus(HttpStatusCode.OK)
else call.respondStatus(HttpStatusCode.NotFound)
}
delete {
val userId = call.userId()
val noteUuid = call.parameters.noteUuid()
if (noteService.delete(userId, noteUuid))
call.respondStatus(HttpStatusCode.OK)
else
call.respondStatus(HttpStatusCode.NotFound)
}
}
}
}

View File

@ -1,31 +1,32 @@
package be.vandewalleh.routing
package be.vandewalleh.routing.notes
import be.vandewalleh.extensions.receiveNoteCreate
import be.vandewalleh.entities.Note
import be.vandewalleh.extensions.userId
import be.vandewalleh.services.NotesService
import be.vandewalleh.services.NoteService
import io.ktor.application.*
import io.ktor.auth.*
import io.ktor.http.*
import io.ktor.request.*
import io.ktor.response.*
import io.ktor.routing.*
import org.kodein.di.Kodein
import org.kodein.di.generic.instance
fun Routing.notes(kodein: Kodein) {
val notesService by kodein.instance<NotesService>()
val noteService by kodein.instance<NoteService>()
authenticate {
get("/notes") {
val userId = call.userId()
val notes = notesService.getNotes(userId)
val notes = noteService.findAll(userId)
call.respond(notes)
}
post("/notes") {
val userId = call.userId()
val note = call.receiveNoteCreate()
val uuid = notesService.createNote(userId, note)
call.respond(HttpStatusCode.Created, mapOf("uuid" to uuid))
val note = call.receive<Note>()
val createdNote = noteService.create(userId, note)
call.respond(HttpStatusCode.Created, createdNote)
}
}
}

View File

@ -1,7 +1,7 @@
package be.vandewalleh.routing
package be.vandewalleh.routing.notes
import be.vandewalleh.extensions.userId
import be.vandewalleh.services.NotesService
import be.vandewalleh.services.NoteService
import io.ktor.application.*
import io.ktor.auth.*
import io.ktor.response.*
@ -10,11 +10,11 @@ import org.kodein.di.Kodein
import org.kodein.di.generic.instance
fun Routing.tags(kodein: Kodein) {
val notesService by kodein.instance<NotesService>()
val noteService by kodein.instance<NoteService>()
authenticate {
get("/tags") {
call.respond(notesService.getTags(call.userId()))
call.respond(noteService.getTags(call.userId()))
}
}
}
}

View File

@ -1,4 +1,4 @@
package be.vandewalleh.routing
package be.vandewalleh.routing.user
import be.vandewalleh.auth.SimpleJWT
import be.vandewalleh.auth.UserDbIdPrincipal

View File

@ -1,4 +1,4 @@
package be.vandewalleh.routing
package be.vandewalleh.routing.user
import be.vandewalleh.extensions.respondStatus
import be.vandewalleh.extensions.userId

View File

@ -0,0 +1,144 @@
package be.vandewalleh.services
import be.vandewalleh.entities.Note
import be.vandewalleh.extensions.ioAsync
import be.vandewalleh.extensions.launchIo
import be.vandewalleh.tables.Notes
import be.vandewalleh.tables.Tags
import me.liuwj.ktorm.database.*
import me.liuwj.ktorm.dsl.*
import me.liuwj.ktorm.entity.*
import org.kodein.di.Kodein
import org.kodein.di.KodeinAware
import org.kodein.di.generic.instance
import java.time.LocalDateTime
import java.util.*
/**
* service to handle database queries at the Notes level.
*/
class NoteService(override val kodein: Kodein) : KodeinAware {
private val db by instance<Database>()
/**
* returns a list of [Note] associated with the userId
*/
suspend fun findAll(userId: Int): List<Note> {
val notes = launchIo {
db.sequenceOf(Notes, withReferences = false)
.filterColumns { it.columns - it.userId - it.content }
.filter { it.userId eq userId }
.sortedByDescending { it.updatedAt }
.toList()
}
if (notes.isEmpty()) return emptyList()
val allTags = launchIo {
db.sequenceOf(Tags, withReferences = false)
.filterColumns { listOf(it.noteUuid, it.name) }
.filter { it.noteUuid inList notes.map { note -> note.uuid } }
.toList()
}
val tagsByUuid = allTags.groupBy({ it.note.uuid }, { it.name })
notes.forEach {
val tags = tagsByUuid[it.uuid]
if (tags != null) it.tags = tags
}
return notes
}
suspend fun exists(userId: Int, uuid: UUID) = launchIo {
db.sequenceOf(Notes, withReferences = false).any { it.userId eq userId and (it.uuid eq uuid) }
}
suspend fun create(userId: Int, note: Note): Note = launchIo {
val uuid = UUID.randomUUID()
val newNote = note.copy().apply {
this["uuid"] = uuid
this.user["id"] = userId
this.updatedAt = LocalDateTime.now()
}
db.useTransaction {
db.sequenceOf(Notes).add(newNote)
db.batchInsert(Tags) {
note.tags.forEach { tagName ->
item {
it.noteUuid to uuid
it.name to tagName
}
}
}
}
newNote
}
@Suppress("UNCHECKED_CAST")
suspend fun find(userId: Int, noteUuid: UUID): Note? {
val deferredNote = ioAsync {
db.sequenceOf(Notes, withReferences = false)
.filterColumns { it.columns - it.userId }
.filter { it.uuid eq noteUuid }
.find { it.userId eq userId }
}
val deferredTags = ioAsync {
db.sequenceOf(Tags, withReferences = false)
.filter { it.noteUuid eq noteUuid }
.mapColumns { it.name } as List<String>
}
val note = deferredNote.await() ?: return null
val tags = deferredTags.await()
return note.also { it.tags = tags }
}
suspend fun updateNote(userId: Int, note: Note): Boolean = launchIo {
if (note["uuid"] == null) error("UUID is required")
db.useTransaction {
val currentNote = db.sequenceOf(Notes, withReferences = false)
.find { it.uuid eq note.uuid and (it.userId eq userId) }
?: return@launchIo false
currentNote.title = note.title
currentNote.content = note.content
currentNote.updatedAt = LocalDateTime.now()
currentNote.flushChanges()
// delete all tags
db.delete(Tags) {
it.noteUuid eq note.uuid
}
// put new ones
note.tags.forEach { tagName ->
db.insert(Tags) {
it.name to tagName
it.noteUuid to note.uuid
}
}
}
true
}
suspend fun delete(userId: Int, noteUuid: UUID): Boolean = launchIo {
db.useTransaction {
db.delete(Notes) { it.uuid eq noteUuid and (it.userId eq userId) } == 1
}
}
@Suppress("UNCHECKED_CAST")
suspend fun getTags(userId: Int): List<String> = launchIo {
db.sequenceOf(Tags)
.filter { it.note.userId eq userId }
.mapColumns(isDistinct = true) { it.name } as List<String>
}
}

View File

@ -1,204 +0,0 @@
package be.vandewalleh.services
import be.vandewalleh.extensions.ioAsync
import be.vandewalleh.tables.Chapters
import be.vandewalleh.tables.Notes
import be.vandewalleh.tables.Tags
import me.liuwj.ktorm.database.*
import me.liuwj.ktorm.dsl.*
import me.liuwj.ktorm.entity.*
import org.kodein.di.Kodein
import org.kodein.di.KodeinAware
import org.kodein.di.generic.instance
import java.time.LocalDateTime
import java.util.*
/**
* service to handle database queries at the Notes level.
*/
class NotesService(override val kodein: Kodein) : KodeinAware {
private val db by instance<Database>()
/**
* returns a list of [BasicNoteDTO] associated with the userId
*/
fun getNotes(userId: Int): List<BasicNoteDTO> {
val notes = db.sequenceOf(Notes, withReferences = false)
.filterColumns { listOf(it.uuid, it.title, it.updatedAt) }
.filter { it.userId eq userId }
.sortedByDescending { it.updatedAt }
.toList()
if (notes.isEmpty()) return emptyList()
val tags = db.sequenceOf(Tags, withReferences = false)
.filterColumns { listOf(it.noteUuid, it.name) }
.filter { it.noteUuid inList notes.map { it.uuid } }
.toList()
return notes.map { note ->
val noteTags = tags.asSequence()
.filter { it.note.uuid == note.uuid }
.map { it.name }
.toList()
BasicNoteDTO(note.uuid, note.title, noteTags, note.updatedAt)
}
}
fun noteExists(userId: Int, uuid: UUID): Boolean {
return db.sequenceOf(Notes)
.filterColumns { listOf(it.uuid) }
.find {
it.userId eq userId and (it.uuid eq uuid)
} == null
}
fun createNote(userId: Int, note: FullNoteCreateDTO): UUID {
val uuid = UUID.randomUUID()
db.useTransaction {
db.insert(Notes) {
it.uuid to uuid
it.title to note.title
it.userId to userId
it.updatedAt to LocalDateTime.now()
}
db.batchInsert(Tags) {
note.tags.forEach { tagName ->
item {
it.noteUuid to uuid
it.name to tagName
}
}
}
db.batchInsert(Chapters) {
note.chapters.forEachIndexed { index, chapter ->
item {
it.noteUuid to uuid
it.title to chapter.title
it.number to index
it.content to chapter.content
}
}
}
}
return uuid
}
suspend fun getNote(userId: Int, noteUuid: UUID): FullNoteDTO? {
val deferredNote = ioAsync {
db.sequenceOf(Notes, withReferences = false)
.filterColumns { listOf(it.title, it.updatedAt) }
.filter { it.uuid eq noteUuid }
.find { it.userId eq userId }
}
val deferredTags = ioAsync {
db.from(Tags)
.select(Tags.name)
.where { Tags.noteUuid eq noteUuid }
.map { it[Tags.name]!! }
.toList()
}
val deferredChapters = ioAsync {
db.from(Chapters)
.select(Chapters.title, Chapters.content)
.where { Chapters.noteUuid eq noteUuid }
.orderBy(Chapters.number.asc())
.map { ChapterDTO(it[Chapters.title]!!, it[Chapters.content]!!) }
.toList()
}
val note = deferredNote.await() ?: return null
val tags = deferredTags.await()
val chapters = deferredChapters.await()
return FullNoteDTO(
uuid = noteUuid,
title = note.title,
updatedAt = note.updatedAt,
tags = tags,
chapters = chapters
)
}
fun updateNote(patch: FullNotePatchDTO) {
if (patch.uuid == null) return
db.useTransaction {
if (patch.title != null) {
db.update(Notes) {
it.title to patch.title
it.updatedAt to LocalDateTime.now()
where { it.uuid eq patch.uuid }
}
}
if (patch.tags != null) {
// delete all tags
db.delete(Tags) {
it.noteUuid eq patch.uuid
}
// put new ones
patch.tags.forEach { tagName ->
db.insert(Tags) {
it.name to tagName
it.noteUuid to patch.uuid
}
}
}
}
TODO("get chapters")
}
fun deleteNote(noteUuid: UUID): Unit =
db.useTransaction {
db.delete(Notes) { it.uuid eq noteUuid }
}
fun getTags(userId: Int): List<String> = db.from(Tags)
.leftJoin(Notes, on = Tags.noteUuid eq Notes.uuid)
.select(Tags.name)
.where { Notes.userId eq userId }
.map { it[Tags.name]!! }
}
data class ChapterDTO(
val title: String,
val content: String
)
data class FullNoteDTO(
val uuid: UUID,
val title: String,
val updatedAt: LocalDateTime,
val tags: List<String>,
val chapters: List<ChapterDTO>
)
data class FullNoteCreateDTO(
val title: String,
val tags: List<String>,
val chapters: List<ChapterDTO>
)
data class FullNotePatchDTO(
val uuid: UUID? = null,
val title: String? = null,
val updatedAt: LocalDateTime? = null,
val tags: List<String>? = null,
val chapters: List<ChapterDTO>? = null
)
data class BasicNoteDTO(
val uuid: UUID,
val title: String,
val tags: List<String>,
val updatedAt: LocalDateTime
)

View File

@ -9,6 +9,6 @@ import org.kodein.di.generic.singleton
* [Kodein] controller module containing the app services
*/
val serviceModule = Kodein.Module(name = "Services") {
bind<NotesService>() with singleton { NotesService(this.kodein) }
bind<NoteService>() with singleton { NoteService(this.kodein) }
bind<UserService>() with singleton { UserService(this.kodein) }
}
}

View File

@ -1,14 +0,0 @@
package be.vandewalleh.tables
import be.vandewalleh.entities.Chapter
import be.vandewalleh.extensions.uuidBinary
import me.liuwj.ktorm.schema.*
object Chapters : Table<Chapter>("Chapters") {
val id = int("id").primaryKey().bindTo { it.id }
val number = int("number").bindTo { it.number }
val content = text("content").bindTo { it.content }
val title = varchar("title").bindTo { it.title }
val noteUuid = uuidBinary("note_uuid").references(Notes) { it.note }
val note get() = noteUuid.referenceTable as Notes
}

View File

@ -4,9 +4,14 @@ import be.vandewalleh.entities.Note
import be.vandewalleh.extensions.uuidBinary
import me.liuwj.ktorm.schema.*
object Notes : Table<Note>("Notes") {
open class Notes(alias: String?) : Table<Note>("Notes", alias) {
companion object : Notes(null)
override fun aliased(alias: String) = Notes(alias)
val uuid = uuidBinary("uuid").primaryKey().bindTo { it.uuid }
val title = varchar("title").bindTo { it.title }
val content = varchar("content").bindTo { it.content }
val userId = int("user_id").references(Users) { it.user }
val updatedAt = datetime("updated_at").bindTo { it.updatedAt }
val user get() = userId.referenceTable as Users

View File

@ -4,7 +4,11 @@ import be.vandewalleh.entities.Tag
import be.vandewalleh.extensions.uuidBinary
import me.liuwj.ktorm.schema.*
object Tags : Table<Tag>("Tags") {
open class Tags(alias: String?) : Table<Tag>("Tags", alias) {
companion object : Tags(null)
override fun aliased(alias: String) = Tags(alias)
val id = int("id").primaryKey().bindTo { it.id }
val name = varchar("name").bindTo { it.name }
val noteUuid = uuidBinary("note_uuid").references(Notes) { it.note }

View File

@ -3,7 +3,11 @@ package be.vandewalleh.tables
import be.vandewalleh.entities.User
import me.liuwj.ktorm.schema.*
object Users : Table<User>("Users") {
open class Users(alias: String?) : Table<User>("Users", alias) {
companion object : Users(null)
override fun aliased(alias: String) = Users(alias)
val id = int("id").primaryKey().bindTo { it.id }
val username = varchar("username").bindTo { it.username }
val password = varchar("password").bindTo { it.password }

View File

@ -0,0 +1,228 @@
package integration.services
import am.ik.yavi.builder.ValidatorBuilder
import am.ik.yavi.core.CustomConstraint
import am.ik.yavi.core.Validator
import be.vandewalleh.entities.Note
import be.vandewalleh.mainModule
import be.vandewalleh.migrations.Migration
import be.vandewalleh.services.NoteService
import be.vandewalleh.services.UserService
import com.fasterxml.jackson.databind.DeserializationFeature
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.databind.util.StdDateFormat
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule
import com.fasterxml.jackson.module.kotlin.readValue
import com.github.javafaker.Faker
import kotlinx.coroutines.runBlocking
import me.liuwj.ktorm.jackson.*
import org.amshove.kluent.*
import org.junit.jupiter.api.*
import org.kodein.di.Kodein
import org.kodein.di.generic.bind
import org.kodein.di.generic.instance
import org.kodein.di.generic.singleton
import utils.KMariadbContainer
import javax.sql.DataSource
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class NoteServiceTest {
@Nested
inner class DB {
private val mariadb = KMariadbContainer().apply { start() }
private val kodein = Kodein {
import(mainModule, allowOverride = true)
bind<DataSource>(overrides = true) with singleton { mariadb.datasource() }
}
init {
val migration by kodein.instance<Migration>()
migration.migrate()
}
@Test
fun run() {
val userService by kodein.instance<UserService>()
val user = runBlocking { userService.create("test", "test")!! }
val noteService by kodein.instance<NoteService>()
val note = runBlocking {
noteService.create(user.id, Note {
this.title = "a note"
this.content = """# Title
|
|😝😝😝😝
|another line
""".trimMargin()
this.tags = listOf("a", "tag")
})
}
println(note)
val objectMapper = ObjectMapper().apply {
registerModule(JavaTimeModule())
registerModule(KtormModule())
disable(DeserializationFeature.ACCEPT_FLOAT_AS_INT)
dateFormat = StdDateFormat()
}
val json = objectMapper.writeValueAsString(note)
println(json)
}
@Test
fun `test tag list`() {
val userService by kodein.instance<UserService>()
val user = runBlocking { userService.create("test", "test")!! }
val user2 = runBlocking { userService.create("user2", "test")!! }
val noteService by kodein.instance<NoteService>()
runBlocking {
noteService.create(user.id, Note {
title = "test"
content = ""
tags = listOf("same")
})
noteService.create(user.id, Note {
title = "test2"
content = ""
tags = listOf("same")
})
noteService.create(user.id, Note {
title = "test3"
content = ""
tags = listOf("another")
})
noteService.create(user2.id, Note {
title = "test"
content = ""
tags = listOf("user2")
})
}
val user1Tags = runBlocking { noteService.getTags(user.id) }
user1Tags `should be equal to` listOf("same", "another")
val user2Tags = runBlocking { noteService.getTags(user2.id) }
user2Tags `should be equal to` listOf("user2")
}
@Test
fun `test patch note`() {
val noteService by kodein.instance<NoteService>()
val userService by kodein.instance<UserService>()
val user = runBlocking { userService.create(Faker().name().username(), "test") }!!
val note = runBlocking {
noteService.create(user.id, Note {
this.title = "title"
this.content = "old content"
this.tags = emptyList()
})
}
val get = runBlocking { noteService.find(user.id, note.uuid) }
runBlocking {
noteService.updateNote(user.id, Note {
uuid = note.uuid
title = "new title"
})
}
val updated = runBlocking { noteService.find(user.id, note.uuid) }
println("updated: $updated")
}
}
@Nested
inner class NoteValidation {
@Test
fun `test update constraints`() {
val fieldPresentConstraint = object : CustomConstraint<Note> {
override fun defaultMessageFormat() = "fmt {0} {1} {2}"
override fun messageKey() = "title|content|tags"
override fun test(note: Note): Boolean {
val hasTitle = note["title"] != null
val hasContent = note["content"] != null
val hasTags = note["tags"] != null && note.tags.isNotEmpty()
return hasTitle || hasContent || hasTags
}
}
val userValidator: Validator<Note> = ValidatorBuilder<Note>()
.constraintOnTarget(fieldPresentConstraint, "present")
.build()
userValidator.validate(Note {
title = "this is a title"
}).isValid `should be equal to` true
userValidator.validate(Note {
content = "this is a title"
}).isValid `should be equal to` true
userValidator.validate(Note {
tags = emptyList()
}).isValid `should be equal to` false
userValidator.validate(Note {
tags = listOf("tags")
}).isValid `should be equal to` true
userValidator.validate(Note {
tags = listOf("tags")
title = "This is a title"
}).isValid `should be equal to` true
userValidator.validate(Note {
tags = listOf("tags")
title = "This is a title"
content = """
# This is
some markdown content
""".trimIndent()
}).isValid `should be equal to` true
userValidator.validate(Note {
tags = listOf("tags")
title = "This is a title"
content = """
# This is
some markdown content
""".trimIndent()
}).isValid `should be equal to` true
userValidator.validate(Note()).isValid `should be equal to` false
}
}
@Nested
inner class NoteEntity {
@Test
fun `test entity`() {
val objectMapper = ObjectMapper().apply {
registerModule(JavaTimeModule())
registerModule(KtormModule())
disable(DeserializationFeature.ACCEPT_FLOAT_AS_INT)
dateFormat = StdDateFormat()
}
val note: Note = objectMapper.readValue("""{"uuid": "c6d80-5fe6-4a30-b034-da63f6663c2c"}""")
println(note.uuid)
println(note.uuid::class.qualifiedName)
println(note.uuid.leastSignificantBits)
println(note.uuid.mostSignificantBits)
}
}
}