diff --git a/api/http/test.http b/api/http/test.http
index c32cf85..9a8f0f9 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/{{uuid}}
+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
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..301e6f4
--- /dev/null
+++ b/api/resources/db/migration/V8__Notes_uuid.sql
@@ -0,0 +1,37 @@
+-- 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 CASCADE ON UPDATE RESTRICT;
+
+CREATE TABLE `Chapters`
+(
+ `id` int PRIMARY KEY AUTO_INCREMENT,
+ `number` int NOT NULL,
+ `title` varchar(50) 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 CASCADE ON UPDATE RESTRICT;
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/entities/Note.kt b/api/src/entities/Note.kt
index f3571a7..09ec45c 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
+ 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..a8a3f3b 100644
--- a/api/src/extensions/ApplicationCallExtensions.kt
+++ b/api/src/extensions/ApplicationCallExtensions.kt
@@ -1,6 +1,8 @@
package be.vandewalleh.extensions
import be.vandewalleh.kodein
+import be.vandewalleh.services.FullNoteCreateDTO
+import be.vandewalleh.services.FullNotePatchDTO
import be.vandewalleh.services.UserService
import io.ktor.application.*
import io.ktor.auth.*
@@ -23,12 +25,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(): FullNoteCreateDTO = 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() : FullNotePatchDTO = receive()
\ No newline at end of file
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())
+}
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/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
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..00ec6a5 100644
--- a/api/src/routing/NotesController.kt
+++ b/api/src/routing/NotesController.kt
@@ -1,9 +1,11 @@
package be.vandewalleh.routing
+import be.vandewalleh.extensions.receiveNoteCreate
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 +20,12 @@ fun Routing.notes(kodein: Kodein) {
val notes = notesService.getNotes(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))
+ }
}
}
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..8c71777 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..38f6d6e 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,175 @@ 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 noteExists(userId: Int, uuid: UUID): Boolean {
+ 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 noteId = db.insertAndGenerateKey(Notes) {
- it.title to title
+ db.insert(Notes) {
+ it.uuid to uuid
+ it.title to note.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) {
+ 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 getTagsAndChapters(noteId: Int): TagsChaptersDTO {
+ fun getNote(noteUuid: UUID): FullNoteDTO {
+ 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.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)
+ 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(noteId: Int, tags: List?, title: String?): Unit =
+ fun updateNote(patch: FullNotePatchDTO) {
+ 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 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,
+ 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
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
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,
},
}