From 2e7e6d5e949a5d143e67a35a281878840fb50d12 Mon Sep 17 00:00:00 2001 From: Hubert Van De Walle Date: Sat, 25 Apr 2020 16:09:23 +0200 Subject: [PATCH 01/11] Add uuidBinary column type --- api/src/extensions/KtormExtensions.kt | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 api/src/extensions/KtormExtensions.kt diff --git a/api/src/extensions/KtormExtensions.kt b/api/src/extensions/KtormExtensions.kt new file mode 100644 index 0000000..2b691e0 --- /dev/null +++ b/api/src/extensions/KtormExtensions.kt @@ -0,0 +1,27 @@ +package be.vandewalleh.extensions + +import me.liuwj.ktorm.schema.* +import java.nio.ByteBuffer +import java.sql.PreparedStatement +import java.sql.ResultSet +import java.sql.Types +import java.util.* + +class UuidBinarySqlType : SqlType(Types.BINARY, typeName = "uuidBinary") { + override fun doGetResult(rs: ResultSet, index: Int): UUID? { + val value = rs.getBytes(index) ?: return null + return ByteBuffer.wrap(value).let { b -> UUID(b.long, b.long) } + } + + override fun doSetParameter(ps: PreparedStatement, index: Int, parameter: UUID) { + val bytes = ByteBuffer.allocate(16) + .putLong(parameter.mostSignificantBits) + .putLong(parameter.leastSignificantBits) + .array() + ps.setBytes(index, bytes) + } +} + +fun BaseTable.uuidBinary(name: String): BaseTable.ColumnRegistration { + return registerColumn(name, UuidBinarySqlType()) +} From 87c4eaed8c4e39da233cd3bc39b499273cb57b16 Mon Sep 17 00:00:00 2001 From: Hubert Van De Walle Date: Sat, 25 Apr 2020 16:15:42 +0200 Subject: [PATCH 02/11] Add migration --- api/resources/db/migration/V8__Notes_uuid.sql | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 api/resources/db/migration/V8__Notes_uuid.sql diff --git a/api/resources/db/migration/V8__Notes_uuid.sql b/api/resources/db/migration/V8__Notes_uuid.sql new file mode 100644 index 0000000..ef36e39 --- /dev/null +++ b/api/resources/db/migration/V8__Notes_uuid.sql @@ -0,0 +1,36 @@ +-- no need to migrate existing data yet +drop table if exists Chapters; +drop table if exists Tags; +drop table if exists Notes; + +CREATE TABLE `Notes` +( + `uuid` binary(16) PRIMARY KEY, + `title` varchar(50) NOT NULL, + `user_id` int NOT NULL, + `updated_at` datetime +); + +ALTER TABLE `Notes` + ADD FOREIGN KEY (`user_id`) REFERENCES `Users` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT; + +CREATE TABLE `Tags` +( + `id` int PRIMARY KEY AUTO_INCREMENT, + `name` varchar(50) NOT NULL, + `note_uuid` binary(16) NOT NULL +); + +ALTER TABLE `Tags` + ADD FOREIGN KEY (`note_uuid`) REFERENCES `Notes` (`uuid`) ON DELETE RESTRICT ON UPDATE RESTRICT; + +CREATE TABLE `Chapters` +( + `id` int PRIMARY KEY AUTO_INCREMENT, + `number` int NOT NULL, + `content` text NOT NULL, + `note_uuid` binary(16) NOT NULL +); + +ALTER TABLE `Chapters` + ADD FOREIGN KEY (`note_uuid`) REFERENCES `Notes` (`uuid`) ON DELETE RESTRICT ON UPDATE RESTRICT; From 573963b1617a2b179949ce81ab2c771770551ded Mon Sep 17 00:00:00 2001 From: Hubert Van De Walle Date: Sat, 25 Apr 2020 16:22:02 +0200 Subject: [PATCH 03/11] Update tables --- api/src/entities/Note.kt | 3 ++- api/src/tables/Chapters.kt | 5 +++-- api/src/tables/Notes.kt | 3 ++- api/src/tables/Tags.kt | 5 +++-- 4 files changed, 10 insertions(+), 6 deletions(-) diff --git a/api/src/entities/Note.kt b/api/src/entities/Note.kt index f3571a7..9f657b0 100644 --- a/api/src/entities/Note.kt +++ b/api/src/entities/Note.kt @@ -2,11 +2,12 @@ package be.vandewalleh.entities import me.liuwj.ktorm.entity.* import java.time.LocalDateTime +import java.util.* interface Note : Entity { companion object : Entity.Factory() - val id: Int + val uuid: UUID var title: String var user: User var updatedAt: LocalDateTime diff --git a/api/src/tables/Chapters.kt b/api/src/tables/Chapters.kt index 51ffb9b..ab04c9d 100644 --- a/api/src/tables/Chapters.kt +++ b/api/src/tables/Chapters.kt @@ -1,6 +1,7 @@ package be.vandewalleh.tables import be.vandewalleh.entities.Chapter +import be.vandewalleh.extensions.uuidBinary import me.liuwj.ktorm.schema.* object Chapters : Table("Chapters") { @@ -8,6 +9,6 @@ object Chapters : Table("Chapters") { val number by int("number").bindTo { it.number } val content by text("content").bindTo { it.content } val title by varchar("title").bindTo { it.title } - val noteId by int("note_id").references(Notes) { it.note } - val note get() = noteId.referenceTable as Notes + val noteUuid by uuidBinary("note_uuid").references(Notes) { it.note } + val note get() = noteUuid.referenceTable as Notes } \ No newline at end of file diff --git a/api/src/tables/Notes.kt b/api/src/tables/Notes.kt index 000dd9f..37ca204 100644 --- a/api/src/tables/Notes.kt +++ b/api/src/tables/Notes.kt @@ -1,10 +1,11 @@ package be.vandewalleh.tables import be.vandewalleh.entities.Note +import be.vandewalleh.extensions.uuidBinary import me.liuwj.ktorm.schema.* object Notes : Table("Notes") { - val id by int("id").primaryKey().bindTo { it.id } + val uuid by uuidBinary("uuid").primaryKey().bindTo { it.uuid } val title by varchar("title").bindTo { it.title } val userId by int("user_id").references(Users) { it.user } val updatedAt by datetime("updated_at").bindTo { it.updatedAt } diff --git a/api/src/tables/Tags.kt b/api/src/tables/Tags.kt index e9e52ef..0f26bd1 100644 --- a/api/src/tables/Tags.kt +++ b/api/src/tables/Tags.kt @@ -1,11 +1,12 @@ package be.vandewalleh.tables import be.vandewalleh.entities.Tag +import be.vandewalleh.extensions.uuidBinary import me.liuwj.ktorm.schema.* object Tags : Table("Tags") { val id by int("id").primaryKey().bindTo { it.id } val name by varchar("name").bindTo { it.name } - val noteId by int("note_id").references(Notes) { it.note } - val note get() = noteId.referenceTable as Notes + val noteUuid by uuidBinary("note_uuid").references(Notes) { it.note } + val note get() = noteUuid.referenceTable as Notes } \ No newline at end of file From 52aae6773fdd683acb0c44e762087dc908d34e34 Mon Sep 17 00:00:00 2001 From: Hubert Van De Walle Date: Sat, 25 Apr 2020 17:24:43 +0200 Subject: [PATCH 04/11] Start migration of notesServices + controllers --- api/src/entities/Note.kt | 2 +- .../extensions/ApplicationCallExtensions.kt | 11 +- api/src/extensions/ParametersExtensions.kt | 16 +-- api/src/routing/ChaptersController.kt | 13 -- api/src/routing/NotesController.kt | 18 +++ api/src/routing/Routes.kt | 1 - api/src/routing/TitleController.kt | 51 ++++--- api/src/services/NotesService.kt | 124 ++++++++++++------ 8 files changed, 130 insertions(+), 106 deletions(-) delete mode 100644 api/src/routing/ChaptersController.kt diff --git a/api/src/entities/Note.kt b/api/src/entities/Note.kt index 9f657b0..09ec45c 100644 --- a/api/src/entities/Note.kt +++ b/api/src/entities/Note.kt @@ -7,7 +7,7 @@ import java.util.* interface Note : Entity { companion object : Entity.Factory() - val uuid: UUID + var uuid: UUID var title: String var user: User var updatedAt: LocalDateTime diff --git a/api/src/extensions/ApplicationCallExtensions.kt b/api/src/extensions/ApplicationCallExtensions.kt index 3a241ce..a09a900 100644 --- a/api/src/extensions/ApplicationCallExtensions.kt +++ b/api/src/extensions/ApplicationCallExtensions.kt @@ -1,6 +1,7 @@ package be.vandewalleh.extensions import be.vandewalleh.kodein +import be.vandewalleh.services.FullNoteDTOPatch import be.vandewalleh.services.UserService import io.ktor.application.* import io.ktor.auth.* @@ -23,12 +24,8 @@ fun ApplicationCall.userId(): Int { return userService.getUserId(email)!! } -private class Tags(val tags: List) +class NoteCreate(val title: String, val tags: List) -suspend fun ApplicationCall.receiveTags(): List { - return receive().tags -} +suspend fun ApplicationCall.receiveNoteCreate(): NoteCreate = receive() -data class NotePatch(val tags: List?, val title: String?) - -suspend fun ApplicationCall.receiveNotePatch() = receive() \ No newline at end of file +suspend fun ApplicationCall.receiveNotePatch() : FullNoteDTOPatch = receive() \ No newline at end of file diff --git a/api/src/extensions/ParametersExtensions.kt b/api/src/extensions/ParametersExtensions.kt index 5e7863b..af7dcd0 100644 --- a/api/src/extensions/ParametersExtensions.kt +++ b/api/src/extensions/ParametersExtensions.kt @@ -1,22 +1,12 @@ package be.vandewalleh.extensions -import be.vandewalleh.kodein -import be.vandewalleh.services.NotesService -import be.vandewalleh.tables.Notes import io.ktor.http.* -import org.kodein.di.generic.instance - -private val notesService by kodein.instance() +import java.util.* fun Parameters.noteTitle(): String { return this["noteTitle"]!! } -/** - * Method that returns a [Notes] ID from it's title and the currently logged in user. - * returns null if none found - */ -fun Parameters.noteId(userId: Int): Int? { - val title = noteTitle() - return notesService.getNoteIdFromUserIdAndTitle(userId, title) +fun Parameters.noteUuid(): UUID { + return UUID.fromString(this["noteUuid"]) } \ No newline at end of file diff --git a/api/src/routing/ChaptersController.kt b/api/src/routing/ChaptersController.kt deleted file mode 100644 index 4236948..0000000 --- a/api/src/routing/ChaptersController.kt +++ /dev/null @@ -1,13 +0,0 @@ -package be.vandewalleh.routing - -import io.ktor.auth.* -import io.ktor.routing.* -import org.kodein.di.Kodein - -fun Routing.chapters(kodein: Kodein) { - authenticate { - route("/notes/{noteTitle}/chapters/{chapterNumber}") { - - } - } -} diff --git a/api/src/routing/NotesController.kt b/api/src/routing/NotesController.kt index ac2e587..e5fc693 100644 --- a/api/src/routing/NotesController.kt +++ b/api/src/routing/NotesController.kt @@ -1,9 +1,13 @@ package be.vandewalleh.routing +import be.vandewalleh.extensions.noteTitle +import be.vandewalleh.extensions.receiveNoteCreate +import be.vandewalleh.extensions.respondStatus import be.vandewalleh.extensions.userId import be.vandewalleh.services.NotesService import io.ktor.application.* import io.ktor.auth.* +import io.ktor.http.* import io.ktor.response.* import io.ktor.routing.* import org.kodein.di.Kodein @@ -18,5 +22,19 @@ fun Routing.notes(kodein: Kodein) { val notes = notesService.getNotes(userId) call.respond(notes) } + + post("/notes") { + val userId = call.userId() + val noteUuid = call.parameters.noteTitle() + val note = call.receiveNoteCreate() + val exists = notesService.noteExistsWithTitle(userId, note.title) + + if (exists) { + return@post call.respondStatus(HttpStatusCode.Conflict) + } + + notesService.createNote(userId, note.title, note.tags) + call.respondStatus(HttpStatusCode.Created) + } } } diff --git a/api/src/routing/Routes.kt b/api/src/routing/Routes.kt index da82d16..4ed1e22 100644 --- a/api/src/routing/Routes.kt +++ b/api/src/routing/Routes.kt @@ -8,6 +8,5 @@ fun Routing.registerRoutes(kodein: Kodein) { login(kodein) notes(kodein) title(kodein) - chapters(kodein) tags(kodein) } \ No newline at end of file diff --git a/api/src/routing/TitleController.kt b/api/src/routing/TitleController.kt index 292e368..fda2058 100644 --- a/api/src/routing/TitleController.kt +++ b/api/src/routing/TitleController.kt @@ -1,6 +1,9 @@ package be.vandewalleh.routing -import be.vandewalleh.extensions.* +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.* import io.ktor.auth.* @@ -14,49 +17,39 @@ fun Routing.title(kodein: Kodein) { val notesService by kodein.instance() authenticate { - route("/notes/{noteTitle}") { - post { - val userId = call.userId() - val title = call.parameters.noteTitle() - val tags = call.receiveTags() - val noteId = call.parameters.noteId(userId) - - if (noteId != null) { - return@post call.respondStatus(HttpStatusCode.Conflict) - } - - notesService.createNote(userId, title, tags) - call.respondStatus(HttpStatusCode.Created) - } - + route("/notes/{noteUuid}") { get { val userId = call.userId() - val noteId = call.parameters.noteId(userId) - ?: return@get call.respondStatus(HttpStatusCode.NotFound) + val noteUuid = call.parameters.noteUuid() - val response = notesService.getTagsAndChapters(noteId) + val exists = notesService.noteExists(userId, noteUuid) + if (exists) return@get call.respondStatus(HttpStatusCode.NotFound) + + val response = notesService.getNote(noteUuid) call.respond(response) } patch { - val notePatch = call.receiveNotePatch() - if (notePatch.tags == null && notePatch.title == null) - return@patch call.respondStatus(HttpStatusCode.BadRequest) - val userId = call.userId() - val noteId = call.parameters.noteId(userId) - ?: return@patch call.respondStatus(HttpStatusCode.NotFound) + val noteUuid = call.parameters.noteUuid() - notesService.updateNote(noteId, notePatch.tags, notePatch.title) + 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 noteId = call.parameters.noteId(userId) - ?: return@delete call.respondStatus(HttpStatusCode.NotFound) + val noteUuid = call.parameters.noteUuid() - notesService.deleteNote(noteId) + 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/services/NotesService.kt b/api/src/services/NotesService.kt index f89c829..9feeda5 100644 --- a/api/src/services/NotesService.kt +++ b/api/src/services/NotesService.kt @@ -5,11 +5,13 @@ 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.time.format.DateTimeFormatter +import java.util.* /** * service to handle database queries at the Notes level. @@ -18,102 +20,140 @@ class NotesService(override val kodein: Kodein) : KodeinAware { private val db by instance() /** - * returns a list of [NotesDTO] associated with the userId + * returns a list of [BasicNoteDTO] associated with the userId */ - fun getNotes(userId: Int): List = db.from(Notes) - .select(Notes.id, Notes.title, Notes.updatedAt) - .where { Notes.userId eq userId } - .orderBy(Notes.updatedAt.desc()) - .map { row -> - val tags = db.from(Tags) - .select(Tags.name) - .where { Tags.noteId eq row[Notes.id]!! } - .map { it[Tags.name]!! } + fun getNotes(userId: Int): List { + val notes = db.sequenceOf(Notes) + .filterColumns { listOf(it.uuid, it.title, it.updatedAt) } + .filter { it.userId eq userId } + .sortedByDescending { it.updatedAt } + .toList() - val updatedAt = DateTimeFormatter.ISO_LOCAL_DATE_TIME.format(row[Notes.updatedAt]!!) + if(notes.isEmpty()) return emptyList() - NotesDTO(row[Notes.title]!!, tags, updatedAt) + val tags = db.sequenceOf(Tags) + .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, DateTimeFormatter.ISO_LOCAL_DATE_TIME.format(note.updatedAt)) } + } - fun getNoteIdFromUserIdAndTitle(userId: Int, noteTitle: String): Int? = db.from(Notes) - .select(Notes.id) - .where { Notes.userId eq userId and (Notes.title eq noteTitle) } - .limit(0, 1) - .map { it[Notes.id]!! } - .firstOrNull() + fun noteExistsWithTitle(userId: Int, title: String): Boolean { + TODO() + } + + fun noteExists(userId: Int, uuid: UUID): Boolean { + TODO() + } fun createNote(userId: Int, title: String, tags: List) { db.useTransaction { - val noteId = db.insertAndGenerateKey(Notes) { + val uuid = UUID.randomUUID() + db.insert(Notes) { + it.uuid to uuid it.title to title it.userId to userId it.updatedAt to LocalDateTime.now() } - tags.forEach { tagName -> - db.insert(Tags) { - it.name to tagName - it.noteId to noteId + db.batchInsert(Tags) { + tags.forEach { tagName -> + item { + it.noteUuid to uuid + it.name to tagName + } } } } } - fun getTagsAndChapters(noteId: Int): TagsChaptersDTO { + fun getNote(noteUuid: UUID): FullNoteDTO { + TODO() val tags = db.from(Tags) .select(Tags.name) - .where { Tags.noteId eq noteId } + .where { Tags.noteUuid eq noteUuid } .map { it[Tags.name]!! } .toList() val chapters = db.from(Chapters) .select(Chapters.title, Chapters.content) - .where { Chapters.noteId eq noteId } + .where { Chapters.noteUuid eq noteUuid } .orderBy(Chapters.number.asc()) - .map { ChaptersDTO(it[Chapters.title]!!, it[Chapters.content]!!) } + .map { ChapterDTO(it[Chapters.title]!!, it[Chapters.content]!!) } .toList() - return TagsChaptersDTO(tags, chapters) } - fun updateNote(noteId: Int, tags: List?, title: String?): Unit = + fun updateNote(patch: FullNoteDTOPatch) { + if(patch.uuid == null) return db.useTransaction { - if (title != null) { + if (patch.title != null) { db.update(Notes) { - it.title to title + it.title to patch.title it.updatedAt to LocalDateTime.now() - where { it.id eq noteId } + where { it.uuid eq patch.uuid } } } - if (tags != null) { + if (patch.tags != null) { // delete all tags db.delete(Tags) { - it.noteId eq noteId + it.noteUuid eq patch.uuid } // put new ones - tags.forEach { tagName -> + patch.tags.forEach { tagName -> db.insert(Tags) { it.name to tagName - it.noteId to noteId + it.noteUuid to patch.uuid } } } } - fun deleteNote(noteId: Int): Unit = + TODO("get chapters") + } + + fun deleteNote(noteUuid: UUID): Unit = db.useTransaction { - db.delete(Notes) { it.id eq noteId } + db.delete(Notes) { it.uuid eq noteUuid } } fun getTags(userId: Int): List = db.from(Tags) - .leftJoin(Notes, on = Tags.noteId eq Notes.id) + .leftJoin(Notes, on = Tags.noteUuid eq Notes.uuid) .select(Tags.name) .where { Notes.userId eq userId } .map { it[Tags.name]!! } } -data class ChaptersDTO(val title: String, val content: String) -data class TagsChaptersDTO(val tags: List, val chapters: List) -data class NotesDTO(val title: String, val tags: List, val updatedAt: String) \ No newline at end of file +data class ChapterDTO(val title: String, val content: String) +data class FullNoteDTO( + val uuid: UUID, + val title: String, + val updatedAt: String, + val tags: List, + val chapters: List +) + +data class FullNoteDTOPatch( + val uuid: UUID? = null, + val title: String? = null, + val updatedAt: String? = null, + val tags: List? = null, + val chapters: List? = null +) + +data class BasicNoteDTO( + val uuid: UUID, + val title: String, + val tags: List, + val updatedAt: String +) \ No newline at end of file From 69ede50a5936bfb9fc00f3cad103da807e409c4b Mon Sep 17 00:00:00 2001 From: Hubert Van De Walle Date: Sat, 25 Apr 2020 18:17:34 +0200 Subject: [PATCH 05/11] Migration done for the backend --- api/resources/db/migration/V8__Notes_uuid.sql | 13 ++-- api/resources/logback.xml | 2 +- .../extensions/ApplicationCallExtensions.kt | 7 +- api/src/routing/NotesController.kt | 13 +--- api/src/routing/TitleController.kt | 2 +- api/src/services/NotesService.kt | 65 ++++++++++++++----- 6 files changed, 65 insertions(+), 37 deletions(-) diff --git a/api/resources/db/migration/V8__Notes_uuid.sql b/api/resources/db/migration/V8__Notes_uuid.sql index ef36e39..f944b38 100644 --- a/api/resources/db/migration/V8__Notes_uuid.sql +++ b/api/resources/db/migration/V8__Notes_uuid.sql @@ -5,9 +5,9 @@ drop table if exists Notes; CREATE TABLE `Notes` ( - `uuid` binary(16) PRIMARY KEY, - `title` varchar(50) NOT NULL, - `user_id` int NOT NULL, + `uuid` binary(16) PRIMARY KEY, + `title` varchar(50) NOT NULL, + `user_id` int NOT NULL, `updated_at` datetime ); @@ -27,9 +27,10 @@ ALTER TABLE `Tags` CREATE TABLE `Chapters` ( `id` int PRIMARY KEY AUTO_INCREMENT, - `number` int NOT NULL, - `content` text NOT NULL, - `note_uuid` binary(16) NOT NULL + `number` int NOT NULL, + `title` varchar(50) NOT NULL, + `content` text NOT NULL, + `note_uuid` binary(16) NOT NULL ); ALTER TABLE `Chapters` diff --git a/api/resources/logback.xml b/api/resources/logback.xml index e573f7d..c19e387 100644 --- a/api/resources/logback.xml +++ b/api/resources/logback.xml @@ -4,7 +4,7 @@ %d{YYYY-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n - + diff --git a/api/src/extensions/ApplicationCallExtensions.kt b/api/src/extensions/ApplicationCallExtensions.kt index a09a900..a8a3f3b 100644 --- a/api/src/extensions/ApplicationCallExtensions.kt +++ b/api/src/extensions/ApplicationCallExtensions.kt @@ -1,7 +1,8 @@ package be.vandewalleh.extensions import be.vandewalleh.kodein -import be.vandewalleh.services.FullNoteDTOPatch +import be.vandewalleh.services.FullNoteCreateDTO +import be.vandewalleh.services.FullNotePatchDTO import be.vandewalleh.services.UserService import io.ktor.application.* import io.ktor.auth.* @@ -26,6 +27,6 @@ fun ApplicationCall.userId(): Int { class NoteCreate(val title: String, val tags: List) -suspend fun ApplicationCall.receiveNoteCreate(): NoteCreate = receive() +suspend fun ApplicationCall.receiveNoteCreate(): FullNoteCreateDTO = receive() -suspend fun ApplicationCall.receiveNotePatch() : FullNoteDTOPatch = receive() \ No newline at end of file +suspend fun ApplicationCall.receiveNotePatch() : FullNotePatchDTO = receive() \ No newline at end of file diff --git a/api/src/routing/NotesController.kt b/api/src/routing/NotesController.kt index e5fc693..00ec6a5 100644 --- a/api/src/routing/NotesController.kt +++ b/api/src/routing/NotesController.kt @@ -1,8 +1,6 @@ package be.vandewalleh.routing -import be.vandewalleh.extensions.noteTitle import be.vandewalleh.extensions.receiveNoteCreate -import be.vandewalleh.extensions.respondStatus import be.vandewalleh.extensions.userId import be.vandewalleh.services.NotesService import io.ktor.application.* @@ -25,16 +23,9 @@ fun Routing.notes(kodein: Kodein) { post("/notes") { val userId = call.userId() - val noteUuid = call.parameters.noteTitle() val note = call.receiveNoteCreate() - val exists = notesService.noteExistsWithTitle(userId, note.title) - - if (exists) { - return@post call.respondStatus(HttpStatusCode.Conflict) - } - - notesService.createNote(userId, note.title, note.tags) - call.respondStatus(HttpStatusCode.Created) + val uuid = notesService.createNote(userId, note) + call.respond(HttpStatusCode.Created, mapOf("uuid" to uuid)) } } } diff --git a/api/src/routing/TitleController.kt b/api/src/routing/TitleController.kt index fda2058..af8e476 100644 --- a/api/src/routing/TitleController.kt +++ b/api/src/routing/TitleController.kt @@ -23,7 +23,7 @@ fun Routing.title(kodein: Kodein) { val noteUuid = call.parameters.noteUuid() val exists = notesService.noteExists(userId, noteUuid) - if (exists) return@get call.respondStatus(HttpStatusCode.NotFound) + if (!exists) return@get call.respondStatus(HttpStatusCode.NotFound) val response = notesService.getNote(noteUuid) call.respond(response) diff --git a/api/src/services/NotesService.kt b/api/src/services/NotesService.kt index 9feeda5..38f6d6e 100644 --- a/api/src/services/NotesService.kt +++ b/api/src/services/NotesService.kt @@ -29,7 +29,7 @@ class NotesService(override val kodein: Kodein) : KodeinAware { .sortedByDescending { it.updatedAt } .toList() - if(notes.isEmpty()) return emptyList() + if (notes.isEmpty()) return emptyList() val tags = db.sequenceOf(Tags) .filterColumns { listOf(it.noteUuid, it.name) } @@ -46,37 +46,54 @@ class NotesService(override val kodein: Kodein) : KodeinAware { } } - fun noteExistsWithTitle(userId: Int, title: String): Boolean { - TODO() - } - fun noteExists(userId: Int, uuid: UUID): Boolean { - TODO() + return db.from(Notes) + .select(Notes.uuid) + .where { Notes.userId eq userId } + .where { Notes.uuid eq uuid } + .limit(0, 1) + .toList().size == 1 } - fun createNote(userId: Int, title: String, tags: List) { + fun createNote(userId: Int, note: FullNoteCreateDTO): UUID { + val uuid = UUID.randomUUID() db.useTransaction { - val uuid = UUID.randomUUID() db.insert(Notes) { it.uuid to uuid - it.title to title + it.title to note.title it.userId to userId it.updatedAt to LocalDateTime.now() } db.batchInsert(Tags) { - tags.forEach { tagName -> + 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 } fun getNote(noteUuid: UUID): FullNoteDTO { - TODO() + val note = db.sequenceOf(Notes) + .filterColumns { listOf(it.title, it.updatedAt) } + .find { it.uuid eq noteUuid } ?: error("Note not found") + val tags = db.from(Tags) .select(Tags.name) .where { Tags.noteUuid eq noteUuid } @@ -90,10 +107,18 @@ class NotesService(override val kodein: Kodein) : KodeinAware { .map { ChapterDTO(it[Chapters.title]!!, it[Chapters.content]!!) } .toList() + val updatedAtFormatted = DateTimeFormatter.ISO_LOCAL_DATE_TIME.format(note.updatedAt) + return FullNoteDTO( + uuid = noteUuid, + title = note.title, + updatedAt = updatedAtFormatted, + tags = tags, + chapters = chapters + ) } - fun updateNote(patch: FullNoteDTOPatch) { - if(patch.uuid == null) return + fun updateNote(patch: FullNotePatchDTO) { + if (patch.uuid == null) return db.useTransaction { if (patch.title != null) { db.update(Notes) { @@ -134,7 +159,11 @@ class NotesService(override val kodein: Kodein) : KodeinAware { .map { it[Tags.name]!! } } -data class ChapterDTO(val title: String, val content: String) +data class ChapterDTO( + val title: String, + val content: String +) + data class FullNoteDTO( val uuid: UUID, val title: String, @@ -143,7 +172,13 @@ data class FullNoteDTO( val chapters: List ) -data class FullNoteDTOPatch( +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: String? = null, From d3f74b30e1526a9780d58e4e61b1624a76ec2b09 Mon Sep 17 00:00:00 2001 From: Hubert Van De Walle Date: Sat, 25 Apr 2020 18:17:41 +0200 Subject: [PATCH 06/11] Update tests --- api/http/test.http | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/api/http/test.http b/api/http/test.http index c32cf85..18e56e1 100644 --- a/api/http/test.http +++ b/api/http/test.http @@ -19,7 +19,43 @@ GET http://localhost:8081/notes Authorization: Bearer {{token}} > {% +client.global.set("uuid", response.body[0].uuid); client.test("Request executed successfully", function() { client.assert(response.status === 200, "Response status is not 200"); }); %} + +### Create note +POST http://localhost:8081/notes +Authorization: Bearer {{token}} +Content-Type: application/json + +{ + "title": "test", + "tags": [ + "Some", + "Tags" + ], + "chapters": [ + { + "title": "Chapter 1", + "content": "# This is some content" + } + ] +} + +> {% +client.test("Request executed successfully", function() { + client.assert(response.status === 201, "Response status is not 201"); +}); +%} + +### Get a note +GET http://localhost:8081/notes/6c4b1524-8cd5-426c-8a07-9a9eb082e9d4 +Authorization: Bearer {{token}} + +> {% +client.test("Request executed successfully", function() { + client.assert(response.status === 200, "Response status is not 200"); +}); +%} \ No newline at end of file From 5b7680e0177318bd34825d9dd354708326a87115 Mon Sep 17 00:00:00 2001 From: Hubert Van De Walle Date: Sat, 25 Apr 2020 18:44:19 +0200 Subject: [PATCH 07/11] Add DELETE cors --- api/src/features/CorsFeature.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/api/src/features/CorsFeature.kt b/api/src/features/CorsFeature.kt index ddc7342..019c172 100644 --- a/api/src/features/CorsFeature.kt +++ b/api/src/features/CorsFeature.kt @@ -9,5 +9,6 @@ fun Application.corsFeature() { anyHost() header(HttpHeaders.ContentType) header(HttpHeaders.Authorization) + methods.add(HttpMethod.Delete) } } \ No newline at end of file From 70e76f964150afee9eeae4048996f41df3349b1b Mon Sep 17 00:00:00 2001 From: Hubert Van De Walle Date: Sat, 25 Apr 2020 18:44:38 +0200 Subject: [PATCH 08/11] Fix cascade --- api/resources/db/migration/V8__Notes_uuid.sql | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/api/resources/db/migration/V8__Notes_uuid.sql b/api/resources/db/migration/V8__Notes_uuid.sql index f944b38..301e6f4 100644 --- a/api/resources/db/migration/V8__Notes_uuid.sql +++ b/api/resources/db/migration/V8__Notes_uuid.sql @@ -22,7 +22,7 @@ CREATE TABLE `Tags` ); ALTER TABLE `Tags` - ADD FOREIGN KEY (`note_uuid`) REFERENCES `Notes` (`uuid`) ON DELETE RESTRICT ON UPDATE RESTRICT; + ADD FOREIGN KEY (`note_uuid`) REFERENCES `Notes` (`uuid`) ON DELETE CASCADE ON UPDATE RESTRICT; CREATE TABLE `Chapters` ( @@ -34,4 +34,4 @@ CREATE TABLE `Chapters` ); ALTER TABLE `Chapters` - ADD FOREIGN KEY (`note_uuid`) REFERENCES `Notes` (`uuid`) ON DELETE RESTRICT ON UPDATE RESTRICT; + ADD FOREIGN KEY (`note_uuid`) REFERENCES `Notes` (`uuid`) ON DELETE CASCADE ON UPDATE RESTRICT; From a9ec03d58f0d91ddcc56d21edbde2492851d06ba Mon Sep 17 00:00:00 2001 From: Hubert Van De Walle Date: Sat, 25 Apr 2020 18:45:01 +0200 Subject: [PATCH 09/11] Fix test --- api/http/test.http | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/http/test.http b/api/http/test.http index 18e56e1..9a8f0f9 100644 --- a/api/http/test.http +++ b/api/http/test.http @@ -51,7 +51,7 @@ client.test("Request executed successfully", function() { %} ### Get a note -GET http://localhost:8081/notes/6c4b1524-8cd5-426c-8a07-9a9eb082e9d4 +GET http://localhost:8081/notes/{{uuid}} Authorization: Bearer {{token}} > {% From f1eed842d265fcd13cf7d80c5ac58337488271f0 Mon Sep 17 00:00:00 2001 From: Hubert Van De Walle Date: Sat, 25 Apr 2020 18:45:15 +0200 Subject: [PATCH 10/11] Fix 404 logic --- api/src/routing/TitleController.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/api/src/routing/TitleController.kt b/api/src/routing/TitleController.kt index af8e476..8c71777 100644 --- a/api/src/routing/TitleController.kt +++ b/api/src/routing/TitleController.kt @@ -34,7 +34,7 @@ fun Routing.title(kodein: Kodein) { val noteUuid = call.parameters.noteUuid() val exists = notesService.noteExists(userId, noteUuid) - if (exists) return@patch call.respondStatus(HttpStatusCode.NotFound) + if (!exists) return@patch call.respondStatus(HttpStatusCode.NotFound) val notePatch = call.receiveNotePatch().copy(uuid = noteUuid) @@ -47,7 +47,7 @@ fun Routing.title(kodein: Kodein) { val noteUuid = call.parameters.noteUuid() val exists = notesService.noteExists(userId, noteUuid) - if (exists) return@delete call.respondStatus(HttpStatusCode.NotFound) + if (!exists) return@delete call.respondStatus(HttpStatusCode.NotFound) notesService.deleteNote(noteUuid) call.respondStatus(HttpStatusCode.OK) From eaccc85c8038dce828afd68a534333f47c2c7134 Mon Sep 17 00:00:00 2001 From: Hubert Van De Walle Date: Sat, 25 Apr 2020 19:05:57 +0200 Subject: [PATCH 11/11] Delete notes from table view --- frontend/pages/notes.vue | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/frontend/pages/notes.vue b/frontend/pages/notes.vue index 586b26a..9e76a41 100644 --- a/frontend/pages/notes.vue +++ b/frontend/pages/notes.vue @@ -70,14 +70,19 @@ export default { ], }), mounted() { - this.$axios.get('/notes').then((e) => { - this.notes = e.data - this.loading = false - }) + this.loadNotes() }, methods: { + async loadNotes() { + await this.$axios.get('/notes').then((e) => { + this.notes = e.data + this.loading = false + }) + }, editItem(item) {}, - deleteItem(item) {}, + async deleteItem(item) { + await this.$axios.delete(`/notes/${item.uuid}`).then(this.loadNotes) + }, format, }, }