diff --git a/api/resources/db/migration/V1__Create_user_table.sql b/api/resources/db/migration/V1__Create_tables.sql similarity index 67% rename from api/resources/db/migration/V1__Create_user_table.sql rename to api/resources/db/migration/V1__Create_tables.sql index 622f670..3c2bdfc 100644 --- a/api/resources/db/migration/V1__Create_user_table.sql +++ b/api/resources/db/migration/V1__Create_tables.sql @@ -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); diff --git a/api/src/NotesApplication.kt b/api/src/NotesApplication.kt index e52d240..474ec83 100644 --- a/api/src/NotesApplication.kt +++ b/api/src/NotesApplication.kt @@ -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() { diff --git a/api/src/entities/Chapter.kt b/api/src/entities/Chapter.kt deleted file mode 100644 index 9b881da..0000000 --- a/api/src/entities/Chapter.kt +++ /dev/null @@ -1,13 +0,0 @@ -package be.vandewalleh.entities - -import me.liuwj.ktorm.entity.* - -interface Chapter : Entity { - companion object : Entity.Factory() - - val id: Int - var number: Int - var title: String - var content: String - var note: Note -} \ No newline at end of file diff --git a/api/src/entities/Note.kt b/api/src/entities/Note.kt index 09ec45c..ea0075e 100644 --- a/api/src/entities/Note.kt +++ b/api/src/entities/Note.kt @@ -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 { var uuid: UUID var title: String - var user: User + var content: String var updatedAt: LocalDateTime -} \ No newline at end of file + + @get:JsonIgnore + var user: User + + // Not part of the Notes table + var tags: List +} diff --git a/api/src/extensions/ApplicationCallExtensions.kt b/api/src/extensions/ApplicationCallExtensions.kt index a170e75..08eecd8 100644 --- a/api/src/extensions/ApplicationCallExtensions.kt +++ b/api/src/extensions/ApplicationCallExtensions.kt @@ -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()!!.id - -class NoteCreate(val title: String, val tags: List) - -suspend fun ApplicationCall.receiveNoteCreate(): FullNoteCreateDTO = receive() - -suspend fun ApplicationCall.receiveNotePatch(): FullNotePatchDTO = receive() diff --git a/api/src/extensions/ParametersExtensions.kt b/api/src/extensions/ParametersExtensions.kt index af7dcd0..10f817b 100644 --- a/api/src/extensions/ParametersExtensions.kt +++ b/api/src/extensions/ParametersExtensions.kt @@ -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"]) -} \ No newline at end of file +} diff --git a/api/src/routing/Routes.kt b/api/src/routing/Routes.kt index 6844529..eeb6075 100644 --- a/api/src/routing/Routes.kt +++ b/api/src/routing/Routes.kt @@ -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) -} \ No newline at end of file +} diff --git a/api/src/routing/TitleController.kt b/api/src/routing/TitleController.kt deleted file mode 100644 index 7bfae54..0000000 --- a/api/src/routing/TitleController.kt +++ /dev/null @@ -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() - - 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) - } - } - } -} diff --git a/api/src/routing/notes/NoteController.kt b/api/src/routing/notes/NoteController.kt new file mode 100644 index 0000000..c02fc02 --- /dev/null +++ b/api/src/routing/notes/NoteController.kt @@ -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() + + 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.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) + } + } + } +} diff --git a/api/src/routing/NotesController.kt b/api/src/routing/notes/NotesController.kt similarity index 53% rename from api/src/routing/NotesController.kt rename to api/src/routing/notes/NotesController.kt index 00ec6a5..330d22d 100644 --- a/api/src/routing/NotesController.kt +++ b/api/src/routing/notes/NotesController.kt @@ -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() + val noteService by kodein.instance() 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() + val createdNote = noteService.create(userId, note) + call.respond(HttpStatusCode.Created, createdNote) } } } diff --git a/api/src/routing/TagController.kt b/api/src/routing/notes/TagController.kt similarity index 60% rename from api/src/routing/TagController.kt rename to api/src/routing/notes/TagController.kt index 6c61003..ba8c13c 100644 --- a/api/src/routing/TagController.kt +++ b/api/src/routing/notes/TagController.kt @@ -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() + val noteService by kodein.instance() authenticate { get("/tags") { - call.respond(notesService.getTags(call.userId())) + call.respond(noteService.getTags(call.userId())) } } -} \ No newline at end of file +} diff --git a/api/src/routing/AuthController.kt b/api/src/routing/user/AuthController.kt similarity index 98% rename from api/src/routing/AuthController.kt rename to api/src/routing/user/AuthController.kt index de0711a..de6a308 100644 --- a/api/src/routing/AuthController.kt +++ b/api/src/routing/user/AuthController.kt @@ -1,4 +1,4 @@ -package be.vandewalleh.routing +package be.vandewalleh.routing.user import be.vandewalleh.auth.SimpleJWT import be.vandewalleh.auth.UserDbIdPrincipal diff --git a/api/src/routing/UserController.kt b/api/src/routing/user/UserController.kt similarity index 97% rename from api/src/routing/UserController.kt rename to api/src/routing/user/UserController.kt index 988e18a..c33fdf8 100644 --- a/api/src/routing/UserController.kt +++ b/api/src/routing/user/UserController.kt @@ -1,4 +1,4 @@ -package be.vandewalleh.routing +package be.vandewalleh.routing.user import be.vandewalleh.extensions.respondStatus import be.vandewalleh.extensions.userId diff --git a/api/src/services/NoteService.kt b/api/src/services/NoteService.kt new file mode 100644 index 0000000..a6bae41 --- /dev/null +++ b/api/src/services/NoteService.kt @@ -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() + + /** + * returns a list of [Note] associated with the userId + */ + suspend fun findAll(userId: Int): List { + 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 + } + + 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 = launchIo { + db.sequenceOf(Tags) + .filter { it.note.userId eq userId } + .mapColumns(isDistinct = true) { it.name } as List + } +} diff --git a/api/src/services/NotesService.kt b/api/src/services/NotesService.kt deleted file mode 100644 index eecdca1..0000000 --- a/api/src/services/NotesService.kt +++ /dev/null @@ -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() - - /** - * returns a list of [BasicNoteDTO] associated with the userId - */ - fun getNotes(userId: Int): List { - 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 = 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, - val chapters: List -) - -data class FullNoteCreateDTO( - val title: String, - val tags: List, - val chapters: List -) - -data class FullNotePatchDTO( - val uuid: UUID? = null, - val title: String? = null, - val updatedAt: LocalDateTime? = null, - val tags: List? = null, - val chapters: List? = null -) - -data class BasicNoteDTO( - val uuid: UUID, - val title: String, - val tags: List, - val updatedAt: LocalDateTime -) diff --git a/api/src/services/Services.kt b/api/src/services/Services.kt index 6db6891..f5fdbe8 100644 --- a/api/src/services/Services.kt +++ b/api/src/services/Services.kt @@ -9,6 +9,6 @@ import org.kodein.di.generic.singleton * [Kodein] controller module containing the app services */ val serviceModule = Kodein.Module(name = "Services") { - bind() with singleton { NotesService(this.kodein) } + bind() with singleton { NoteService(this.kodein) } bind() with singleton { UserService(this.kodein) } -} \ No newline at end of file +} diff --git a/api/src/tables/Chapters.kt b/api/src/tables/Chapters.kt deleted file mode 100644 index 8f4e841..0000000 --- a/api/src/tables/Chapters.kt +++ /dev/null @@ -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("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 -} diff --git a/api/src/tables/Notes.kt b/api/src/tables/Notes.kt index 3078fff..a89cbfb 100644 --- a/api/src/tables/Notes.kt +++ b/api/src/tables/Notes.kt @@ -4,9 +4,14 @@ import be.vandewalleh.entities.Note import be.vandewalleh.extensions.uuidBinary import me.liuwj.ktorm.schema.* -object Notes : Table("Notes") { +open class Notes(alias: String?) : Table("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 diff --git a/api/src/tables/Tags.kt b/api/src/tables/Tags.kt index e173137..6edbd8a 100644 --- a/api/src/tables/Tags.kt +++ b/api/src/tables/Tags.kt @@ -4,7 +4,11 @@ import be.vandewalleh.entities.Tag import be.vandewalleh.extensions.uuidBinary import me.liuwj.ktorm.schema.* -object Tags : Table("Tags") { +open class Tags(alias: String?) : Table("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 } diff --git a/api/src/tables/Users.kt b/api/src/tables/Users.kt index 1150785..2136fa8 100644 --- a/api/src/tables/Users.kt +++ b/api/src/tables/Users.kt @@ -3,7 +3,11 @@ package be.vandewalleh.tables import be.vandewalleh.entities.User import me.liuwj.ktorm.schema.* -object Users : Table("Users") { +open class Users(alias: String?) : Table("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 } diff --git a/api/test/integration/services/NoteServiceTest.kt b/api/test/integration/services/NoteServiceTest.kt new file mode 100644 index 0000000..d22758f --- /dev/null +++ b/api/test/integration/services/NoteServiceTest.kt @@ -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(overrides = true) with singleton { mariadb.datasource() } + } + + init { + val migration by kodein.instance() + migration.migrate() + } + + @Test + fun run() { + val userService by kodein.instance() + val user = runBlocking { userService.create("test", "test")!! } + val noteService by kodein.instance() + 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() + val user = runBlocking { userService.create("test", "test")!! } + val user2 = runBlocking { userService.create("user2", "test")!! } + + val noteService by kodein.instance() + 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() + val userService by kodein.instance() + 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 { + 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 = ValidatorBuilder() + .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) + } + } + + +}