Merge branch 'feature/notes-notetitle-controller'
This commit is contained in:
commit
efa732ba4a
@ -54,7 +54,6 @@
|
|||||||
|
|
||||||
+ Response 200 (application/json)
|
+ Response 200 (application/json)
|
||||||
+ Attributes (object)
|
+ Attributes (object)
|
||||||
+ title: Kotlin (string)
|
|
||||||
+ tags: Dev, Server (array[string])
|
+ tags: Dev, Server (array[string])
|
||||||
+ chapters (array)
|
+ chapters (array)
|
||||||
+ (Chapter)
|
+ (Chapter)
|
||||||
|
|||||||
@ -11,4 +11,21 @@ Content-Type: application/json
|
|||||||
|
|
||||||
### Get notes
|
### Get notes
|
||||||
GET http://localhost:8081/notes
|
GET http://localhost:8081/notes
|
||||||
Authorization: Bearer {{token}}
|
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}}
|
||||||
|
|||||||
2
api/resources/db/migration/V6__Update_chapter_table.sql
Normal file
2
api/resources/db/migration/V6__Update_chapter_table.sql
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
ALTER TABLE `Chapters`
|
||||||
|
ADD COLUMN `title` varchar(50);
|
||||||
@ -15,4 +15,5 @@ val controllerModule = Kodein.Module(name = "Controller") {
|
|||||||
bind<KodeinController>().inSet() with singleton { UserController(this.kodein) }
|
bind<KodeinController>().inSet() with singleton { UserController(this.kodein) }
|
||||||
bind<KodeinController>().inSet() with singleton { HealthCheckController(this.kodein) }
|
bind<KodeinController>().inSet() with singleton { HealthCheckController(this.kodein) }
|
||||||
bind<KodeinController>().inSet() with singleton { NotesController(this.kodein) }
|
bind<KodeinController>().inSet() with singleton { NotesController(this.kodein) }
|
||||||
|
bind<KodeinController>().inSet() with singleton { NotesTitleController(this.kodein) }
|
||||||
}
|
}
|
||||||
@ -1,5 +1,8 @@
|
|||||||
package be.vandewalleh.controllers
|
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 io.ktor.routing.Routing
|
||||||
import org.kodein.di.Kodein
|
import org.kodein.di.Kodein
|
||||||
import org.kodein.di.KodeinAware
|
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.
|
* Method that subtypes must override to register the handled [Routing] routes.
|
||||||
*/
|
*/
|
||||||
abstract fun Routing.registerRoutes()
|
abstract fun Routing.registerRoutes()
|
||||||
|
|
||||||
|
suspend fun ApplicationCall.respondStatus(status: HttpStatusCode) {
|
||||||
|
this.respond(status, mapOf("message" to status.description))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
170
api/src/controllers/NotesTitleController.kt
Normal file
170
api/src/controllers/NotesTitleController.kt
Normal file
@ -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<Database>()
|
||||||
|
|
||||||
|
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<String>)
|
||||||
|
|
||||||
|
private class ChapterDto(val title: String, val content: String)
|
||||||
|
private class GetResponseBody(val tags: List<String>, val chapters: List<ChapterDto>)
|
||||||
|
|
||||||
|
private class PatchRequestBody(val title: String? = null, val tags: List<String>? = null)
|
||||||
|
|
||||||
|
override val route: Route.() -> Unit = {
|
||||||
|
post {
|
||||||
|
val title = call.noteTitle() ?: error("")
|
||||||
|
val tags = call.receive<PostRequestBody>().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<PatchRequestBody>()
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -7,6 +7,7 @@ interface Chapter : Entity<Chapter> {
|
|||||||
|
|
||||||
val id: Int
|
val id: Int
|
||||||
var number: Int
|
var number: Int
|
||||||
|
var title: String
|
||||||
var content: String
|
var content: String
|
||||||
var note: Note
|
var note: Note
|
||||||
}
|
}
|
||||||
@ -11,6 +11,7 @@ object Chapters : Table<Chapter>("Chapters") {
|
|||||||
val id by int("id").primaryKey().bindTo { it.id }
|
val id by int("id").primaryKey().bindTo { it.id }
|
||||||
val number by int("number").bindTo { it.number }
|
val number by int("number").bindTo { it.number }
|
||||||
val content by text("content").bindTo { it.content }
|
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 noteId by int("note_id").references(Notes) { it.note }
|
||||||
val note get() = noteId.referenceTable as Notes
|
val note get() = noteId.referenceTable as Notes
|
||||||
}
|
}
|
||||||
@ -113,7 +113,6 @@
|
|||||||
</span>}
|
</span>}
|
||||||
</span>}</code></pre><div style="height: 1px;"></div></div></div><div class="tabs"><div class="example-names"><span>Responses</span><span class="tab-button">201</span><span class="tab-button">409</span></div><div class="tab"><div><div class="inner"><div class="description text-muted">This response has no content.</div><div style="height: 1px;"></div></div></div></div><div class="tab"><div><div class="inner"><div class="description text-muted">This response has no content.</div><div style="height: 1px;"></div></div></div></div></div></div></div></div><div class="middle"><div id="notes-note-post" class="action post"><h4 class="action-heading"><div class="name">Create a Note</div><a href="#notes-note-post" class="method post">POST</a><code class="uri">/notes/{noteTitle}</code></h4><div class="title"><strong>URI Parameters</strong><div class="collapse-button show"><span class="close">Hide</span><span class="open">Show</span></div></div><div class="collapse-content"><dl class="inner"><dt>noteTitle</dt><dd><code>string</code> <span class="required">(required)</span> <span class="text-muted example"><strong>Example: </strong><span>Kotlin</span></span><p>The title of the Note.</p>
|
</span>}</code></pre><div style="height: 1px;"></div></div></div><div class="tabs"><div class="example-names"><span>Responses</span><span class="tab-button">201</span><span class="tab-button">409</span></div><div class="tab"><div><div class="inner"><div class="description text-muted">This response has no content.</div><div style="height: 1px;"></div></div></div></div><div class="tab"><div><div class="inner"><div class="description text-muted">This response has no content.</div><div style="height: 1px;"></div></div></div></div></div></div></div></div><div class="middle"><div id="notes-note-post" class="action post"><h4 class="action-heading"><div class="name">Create a Note</div><a href="#notes-note-post" class="method post">POST</a><code class="uri">/notes/{noteTitle}</code></h4><div class="title"><strong>URI Parameters</strong><div class="collapse-button show"><span class="close">Hide</span><span class="open">Show</span></div></div><div class="collapse-content"><dl class="inner"><dt>noteTitle</dt><dd><code>string</code> <span class="required">(required)</span> <span class="text-muted example"><strong>Example: </strong><span>Kotlin</span></span><p>The title of the Note.</p>
|
||||||
</dd></dl></div></div></div><hr class="split"><div class="right"><div class="definition"><span class="method get">GET</span> <span class="uri"><span class="hostname">http://localhost:5000</span>/notes/<span class="hljs-attribute" title="noteTitle">Kotlin</span></span></div><div class="tabs"><div class="example-names"><span>Requests</span><span class="tab-button">example 1</span></div><div class="tab"><div><div class="inner"><h5>Headers</h5><pre><code><span class="hljs-attribute">Content-Type</span>: <span class="hljs-string">application/json</span><br><span class="hljs-attribute">Authorization</span>: <span class="hljs-string">Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c</span></code></pre><div style="height: 1px;"></div></div></div><div class="tabs"><div class="example-names"><span>Responses</span><span class="tab-button">200</span><span class="tab-button">404</span></div><div class="tab"><div><div class="inner"><h5>Headers</h5><pre><code><span class="hljs-attribute">Content-Type</span>: <span class="hljs-string">application/json</span></code></pre><div style="height: 1px;"></div><h5>Body</h5><pre><code>{
|
</dd></dl></div></div></div><hr class="split"><div class="right"><div class="definition"><span class="method get">GET</span> <span class="uri"><span class="hostname">http://localhost:5000</span>/notes/<span class="hljs-attribute" title="noteTitle">Kotlin</span></span></div><div class="tabs"><div class="example-names"><span>Requests</span><span class="tab-button">example 1</span></div><div class="tab"><div><div class="inner"><h5>Headers</h5><pre><code><span class="hljs-attribute">Content-Type</span>: <span class="hljs-string">application/json</span><br><span class="hljs-attribute">Authorization</span>: <span class="hljs-string">Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c</span></code></pre><div style="height: 1px;"></div></div></div><div class="tabs"><div class="example-names"><span>Responses</span><span class="tab-button">200</span><span class="tab-button">404</span></div><div class="tab"><div><div class="inner"><h5>Headers</h5><pre><code><span class="hljs-attribute">Content-Type</span>: <span class="hljs-string">application/json</span></code></pre><div style="height: 1px;"></div><h5>Body</h5><pre><code>{
|
||||||
"<span class="hljs-attribute">title</span>": <span class="hljs-value"><span class="hljs-string">"Kotlin"</span></span>,
|
|
||||||
"<span class="hljs-attribute">tags</span>": <span class="hljs-value">[
|
"<span class="hljs-attribute">tags</span>": <span class="hljs-value">[
|
||||||
<span class="hljs-string">"Dev"</span>,
|
<span class="hljs-string">"Dev"</span>,
|
||||||
<span class="hljs-string">"Server"</span>
|
<span class="hljs-string">"Server"</span>
|
||||||
@ -132,9 +131,6 @@
|
|||||||
"<span class="hljs-attribute">$schema</span>": <span class="hljs-value"><span class="hljs-string">"http://json-schema.org/draft-04/schema#"</span></span>,
|
"<span class="hljs-attribute">$schema</span>": <span class="hljs-value"><span class="hljs-string">"http://json-schema.org/draft-04/schema#"</span></span>,
|
||||||
"<span class="hljs-attribute">type</span>": <span class="hljs-value"><span class="hljs-string">"object"</span></span>,
|
"<span class="hljs-attribute">type</span>": <span class="hljs-value"><span class="hljs-string">"object"</span></span>,
|
||||||
"<span class="hljs-attribute">properties</span>": <span class="hljs-value">{
|
"<span class="hljs-attribute">properties</span>": <span class="hljs-value">{
|
||||||
"<span class="hljs-attribute">title</span>": <span class="hljs-value">{
|
|
||||||
"<span class="hljs-attribute">type</span>": <span class="hljs-value"><span class="hljs-string">"string"</span>
|
|
||||||
</span>}</span>,
|
|
||||||
"<span class="hljs-attribute">tags</span>": <span class="hljs-value">{
|
"<span class="hljs-attribute">tags</span>": <span class="hljs-value">{
|
||||||
"<span class="hljs-attribute">type</span>": <span class="hljs-value"><span class="hljs-string">"array"</span>
|
"<span class="hljs-attribute">type</span>": <span class="hljs-value"><span class="hljs-string">"array"</span>
|
||||||
</span>}</span>,
|
</span>}</span>,
|
||||||
@ -221,7 +217,7 @@
|
|||||||
"<span class="hljs-attribute">type</span>": <span class="hljs-value"><span class="hljs-string">"array"</span>
|
"<span class="hljs-attribute">type</span>": <span class="hljs-value"><span class="hljs-string">"array"</span>
|
||||||
</span>}
|
</span>}
|
||||||
</span>}
|
</span>}
|
||||||
</span>}</code></pre><div style="height: 1px;"></div></div></div></div></div></div></div></div><div class="middle"><div id="tags-tags-get" class="action get"><h4 class="action-heading"><div class="name">Get all tags</div><a href="#tags-tags-get" class="method get">GET</a><code class="uri">/tags</code></h4></div></div><hr class="split"><div class="middle"><p style="text-align: center;" class="text-muted">Generated by <a href="https://github.com/danielgtaylor/aglio" class="aglio">aglio</a> on 19 Apr 2020</p></div></div></div></div><script>/* eslint-env browser */
|
</span>}</code></pre><div style="height: 1px;"></div></div></div></div></div></div></div></div><div class="middle"><div id="tags-tags-get" class="action get"><h4 class="action-heading"><div class="name">Get all tags</div><a href="#tags-tags-get" class="method get">GET</a><code class="uri">/tags</code></h4></div></div><hr class="split"><div class="middle"><p style="text-align: center;" class="text-muted">Generated by <a href="https://github.com/danielgtaylor/aglio" class="aglio">aglio</a> on 20 Apr 2020</p></div></div></div></div><script>/* eslint-env browser */
|
||||||
/* eslint quotes: [2, "single"] */
|
/* eslint quotes: [2, "single"] */
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user