diff --git a/api-doc/notes/index.apib b/api-doc/notes/index.apib index 95a9892..2ca1111 100644 --- a/api-doc/notes/index.apib +++ b/api-doc/notes/index.apib @@ -54,7 +54,6 @@ + Response 200 (application/json) + Attributes (object) - + title: Kotlin (string) + tags: Dev, Server (array[string]) + chapters (array) + (Chapter) diff --git a/api/http/test.http b/api/http/test.http index c75f3f6..6af1fc7 100644 --- a/api/http/test.http +++ b/api/http/test.http @@ -11,4 +11,21 @@ Content-Type: application/json ### Get notes GET http://localhost:8081/notes -Authorization: Bearer {{token}} \ No newline at end of file +Authorization: Bearer {{token}} + +### Create a note +POST http://localhost:8081/notes/babar +Content-Type: application/json +Authorization: Bearer {{token}} + +{ + "tags": [ + "Test", + "Dev" + ] +} + +### Create a note +GET http://localhost:8081/notes/babar +Content-Type: application/json +Authorization: Bearer {{token}} diff --git a/api/resources/db/migration/V6__Update_chapter_table.sql b/api/resources/db/migration/V6__Update_chapter_table.sql new file mode 100644 index 0000000..1478385 --- /dev/null +++ b/api/resources/db/migration/V6__Update_chapter_table.sql @@ -0,0 +1,2 @@ +ALTER TABLE `Chapters` + ADD COLUMN `title` varchar(50); diff --git a/api/src/controllers/Controllers.kt b/api/src/controllers/Controllers.kt index f4fc0b1..799361c 100644 --- a/api/src/controllers/Controllers.kt +++ b/api/src/controllers/Controllers.kt @@ -15,4 +15,5 @@ val controllerModule = Kodein.Module(name = "Controller") { bind().inSet() with singleton { UserController(this.kodein) } bind().inSet() with singleton { HealthCheckController(this.kodein) } bind().inSet() with singleton { NotesController(this.kodein) } + bind().inSet() with singleton { NotesTitleController(this.kodein) } } \ No newline at end of file diff --git a/api/src/controllers/KodeinController.kt b/api/src/controllers/KodeinController.kt index 332feec..5231c49 100644 --- a/api/src/controllers/KodeinController.kt +++ b/api/src/controllers/KodeinController.kt @@ -1,5 +1,8 @@ package be.vandewalleh.controllers +import io.ktor.application.ApplicationCall +import io.ktor.http.HttpStatusCode +import io.ktor.response.respond import io.ktor.routing.Routing import org.kodein.di.Kodein import org.kodein.di.KodeinAware @@ -11,4 +14,8 @@ abstract class KodeinController(override val kodein: Kodein) : KodeinAware { * Method that subtypes must override to register the handled [Routing] routes. */ abstract fun Routing.registerRoutes() + + suspend fun ApplicationCall.respondStatus(status: HttpStatusCode) { + this.respond(status, mapOf("message" to status.description)) + } } diff --git a/api/src/controllers/NotesTitleController.kt b/api/src/controllers/NotesTitleController.kt new file mode 100644 index 0000000..fdb23ea --- /dev/null +++ b/api/src/controllers/NotesTitleController.kt @@ -0,0 +1,170 @@ +package be.vandewalleh.controllers + +import be.vandewalleh.entities.Note +import be.vandewalleh.entities.Tag +import be.vandewalleh.entities.User +import be.vandewalleh.tables.Chapters +import be.vandewalleh.tables.Notes +import be.vandewalleh.tables.Tags +import be.vandewalleh.tables.Users +import io.ktor.application.ApplicationCall +import io.ktor.application.call +import io.ktor.http.HttpStatusCode +import io.ktor.request.receive +import io.ktor.response.respond +import io.ktor.routing.* +import me.liuwj.ktorm.database.Database +import me.liuwj.ktorm.dsl.* +import me.liuwj.ktorm.entity.add +import me.liuwj.ktorm.entity.find +import me.liuwj.ktorm.entity.sequenceOf +import org.kodein.di.Kodein +import org.kodein.di.generic.instance +import java.time.LocalDateTime + +class NotesTitleController(kodein: Kodein) : AuthCrudController("/notes/{noteTitle}", kodein) { + private val db by kodein.instance() + + private fun ApplicationCall.noteTitle(): String? { + return this.parameters["noteTitle"]!! + } + + private fun ApplicationCall.user(): User { + return db.sequenceOf(Users) + .find { it.email eq this.userEmail() } + ?: error("") + } + + /** + * Method that returns a [Notes] ID from it's title and the currently logged in user. + * returns null if none found + */ + private fun ApplicationCall.requestedNoteId(): Int? { + val user = user() + val title = noteTitle() ?: error("title missing") + + return db.from(Notes) + .select(Notes.id) + .where { Notes.userId eq user.id and (Notes.title eq title) } + .limit(0, 1) + .map { it[Notes.id]!! } + .firstOrNull() + } + + private class PostRequestBody(val tags: List) + + private class ChapterDto(val title: String, val content: String) + private class GetResponseBody(val tags: List, val chapters: List) + + private class PatchRequestBody(val title: String? = null, val tags: List? = null) + + override val route: Route.() -> Unit = { + post { + val title = call.noteTitle() ?: error("") + val tags = call.receive().tags + + val user = call.user() + + val exists = call.requestedNoteId() != null + + if (exists) { + return@post call.respondStatus(HttpStatusCode.Conflict) + } + + db.useTransaction { + val note = Note { + this.title = title + this.user = user + this.updatedAt = LocalDateTime.now() + } + + db.sequenceOf(Notes).add(note) + + tags.forEach { tagName -> + val tag = Tag { + this.note = note + this.name = tagName + } + + db.sequenceOf(Tags).add(tag) + } + } + + call.respondStatus(HttpStatusCode.Created) + } + + get { + val noteId = call.requestedNoteId() + ?: return@get call.respondStatus(HttpStatusCode.NotFound) + + val tags = db.from(Tags) + .select(Tags.name) + .where { Tags.noteId eq noteId } + .map { it[Tags.name]!! } + .toList() + + val chapters = db.from(Chapters) + .select(Chapters.title, Chapters.content) + .where { Chapters.noteId eq noteId } + .orderBy(Chapters.number.asc()) + .map { ChapterDto(it[Chapters.title]!!, it[Chapters.content]!!) } + .toList() + + val response = GetResponseBody(tags, chapters) + + call.respond(response) + } + + patch { + val requestedChanges = call.receive() + + // This means no changes have been requested.. + if (requestedChanges.tags == null && requestedChanges.title == null) { + return@patch call.respondStatus(HttpStatusCode.BadRequest) + } + + val noteId = call.requestedNoteId() + ?: return@patch call.respondStatus(HttpStatusCode.NotFound) + + db.useTransaction { + if (requestedChanges.title != null) { + db.update(Notes) { + it.title to requestedChanges.title + where { it.id eq noteId } + } + } + + if (requestedChanges.tags != null) { + // delete all tags + db.delete(Tags) { + it.noteId eq noteId + } + + // put new ones + requestedChanges.tags.forEach { tagName -> + db.insert(Tags) { + it.name to tagName + it.noteId to noteId + } + } + } + + } + + call.respondStatus(HttpStatusCode.OK) + } + + delete { + val noteId = call.requestedNoteId() + ?: return@delete call.respondStatus(HttpStatusCode.NotFound) + + db.useTransaction { + db.delete(Tags) { it.noteId eq noteId } + db.delete(Chapters) { it.noteId eq noteId } + db.delete(Notes) { it.id eq noteId } + } + + call.respondStatus(HttpStatusCode.OK) + } + } +} \ No newline at end of file diff --git a/api/src/entities/Chapter.kt b/api/src/entities/Chapter.kt index 6ca869d..0498139 100644 --- a/api/src/entities/Chapter.kt +++ b/api/src/entities/Chapter.kt @@ -7,6 +7,7 @@ interface Chapter : Entity { 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/tables/Chapters.kt b/api/src/tables/Chapters.kt index a8d7c8f..4651fda 100644 --- a/api/src/tables/Chapters.kt +++ b/api/src/tables/Chapters.kt @@ -11,6 +11,7 @@ object Chapters : Table("Chapters") { val id by int("id").primaryKey().bindTo { it.id } 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 } \ No newline at end of file diff --git a/public/index.html b/public/index.html index 295d5b7..c840ea6 100644 --- a/public/index.html +++ b/public/index.html @@ -113,7 +113,6 @@ } }
Responses201409
This response has no content.
This response has no content.

Create a Note
POST/notes/{noteTitle}

URI Parameters
HideShow
noteTitle
string (required) Example: Kotlin

The title of the Note.


GET http://localhost:5000/notes/Kotlin
Requestsexample 1
Headers
Content-Type: application/json
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
Responses200404
Headers
Content-Type: application/json
Body
{
-  "title": "Kotlin",
   "tags": [
     "Dev",
     "Server"
@@ -132,9 +131,6 @@
   "$schema": "http://json-schema.org/draft-04/schema#",
   "type": "object",
   "properties": {
-    "title": {
-      "type": "string"
-    },
     "tags": {
       "type": "array"
     },
@@ -221,7 +217,7 @@
       "type": "array"
     }
   }
-}

Get all tags
GET/tags


Generated by aglio on 19 Apr 2020