diff --git a/app/src/main/kotlin/controllers/NoteController.kt b/app/src/main/kotlin/controllers/NoteController.kt index 5bc2f1c..d81b16d 100644 --- a/app/src/main/kotlin/controllers/NoteController.kt +++ b/app/src/main/kotlin/controllers/NoteController.kt @@ -55,10 +55,10 @@ class NoteController( val noteUuid = request.uuidPath() ?: return Response(NOT_FOUND) if (request.method == Method.POST && request.form("delete") != null) { - return if (noteService.delete(jwtPayload.userId, noteUuid)) - Response.redirect("/notes") // FIXME: flash cookie to show success ? + return if (noteService.trash(jwtPayload.userId, noteUuid)) + Response.redirect("/notes") // TODO: flash cookie to show success ? else - Response(NOT_FOUND) // FIXME: show an error + Response(NOT_FOUND) // TODO: show an error } val note = noteService.find(jwtPayload.userId, noteUuid) ?: return Response(NOT_FOUND) @@ -80,7 +80,9 @@ class NoteController( val html = when (it) { MissingMeta -> view.noteEditor(jwtPayload, error = "Missing note metadata", textarea = markdownForm) InvalidMeta -> view.noteEditor(jwtPayload, error = "Invalid note metadata", textarea = markdownForm) - is ValidationError -> view.noteEditor(jwtPayload, validationErrors = it.validationErrors, textarea = markdownForm) + is ValidationError -> view.noteEditor(jwtPayload, + validationErrors = it.validationErrors, + textarea = markdownForm) } Response(BAD_REQUEST).html(html) }, @@ -90,6 +92,26 @@ class NoteController( ) } + fun trash(request: Request, jwtPayload: JwtPayload): Response { + val currentPage = request.query("page")?.toIntOrNull()?.let(::abs) ?: 1 + val tag = request.query("tag") + val (pages, notes) = noteService.paginatedNotes(jwtPayload.userId, currentPage, tag = tag, deleted = true) + return Response(OK).html(view.trash(jwtPayload, notes, currentPage, pages)) + } + + fun deleted(request: Request, jwtPayload: JwtPayload): Response { + val uuid = request.uuidPath() ?: return Response(NOT_FOUND) + return if (request.form("delete") != null) + if (noteService.delete(jwtPayload.userId, uuid)) + Response.redirect("/notes/trash") + else + Response(NOT_FOUND) + else if (noteService.restore(jwtPayload.userId, uuid)) + Response.redirect("/notes/$uuid") + else + Response(NOT_FOUND) + } + private fun Request.uuidPath(): UUID? { val uuidPath = path("uuid")!! return try { diff --git a/app/src/main/kotlin/routes/Router.kt b/app/src/main/kotlin/routes/Router.kt index 9a81d2a..9e33d75 100644 --- a/app/src/main/kotlin/routes/Router.kt +++ b/app/src/main/kotlin/routes/Router.kt @@ -48,10 +48,12 @@ class Router( "/notes" bind GET to { protected(it, noteController::list) }, "/notes/new" bind GET to { protected(it, noteController::new) }, "/notes/new" bind POST to { protected(it, noteController::new) }, + "/notes/trash" bind GET to { protected(it, noteController::trash) }, "/notes/{uuid}" bind GET to { protected(it, noteController::note) }, "/notes/{uuid}" bind POST to { protected(it, noteController::note) }, "/notes/{uuid}/edit" bind GET to { protected(it, noteController::edit) }, "/notes/{uuid}/edit" bind POST to { protected(it, noteController::edit) }, + "/notes/deleted/{uuid}" bind POST to { protected(it, noteController::deleted) }, ) val routes = routes( diff --git a/app/src/main/kotlin/views/NoteView.kt b/app/src/main/kotlin/views/NoteView.kt index 3c0aa71..75d5cff 100644 --- a/app/src/main/kotlin/views/NoteView.kt +++ b/app/src/main/kotlin/views/NoteView.kt @@ -1,9 +1,7 @@ package be.simplenotes.app.views import be.simplenotes.app.utils.StaticFileResolver -import be.simplenotes.app.views.components.Alert -import be.simplenotes.app.views.components.alert -import be.simplenotes.app.views.components.submitButton +import be.simplenotes.app.views.components.* import be.simplenotes.domain.model.PersistedNote import be.simplenotes.domain.model.PersistedNoteMetadata import be.simplenotes.domain.security.JwtPayload @@ -53,41 +51,57 @@ class NoteView(staticFileResolver: StaticFileResolver) : View(staticFileResolver notes: List, currentPage: Int, numberOfPages: Int, - tag: String? - ) = - renderPage(title = "Notes", jwtPayload = jwtPayload) { - div("container mx-auto p-4") { - div("flex justify-between mb-4") { - h1("text-2xl underline") { +"Notes" } + tag: String?, + ) = renderPage(title = "Notes", jwtPayload = jwtPayload) { + div("container mx-auto p-4") { + div("flex justify-between mb-4") { + h1("text-2xl underline") { +"Notes" } + span { + a( + href = "/notes/trash", + classes = "underline font-semibold" + ) { +"Trash" } a( href = "/notes/new", - classes = "btn btn-green" + classes = "ml-2 btn btn-green" ) { +"New" } } - if (notes.isNotEmpty()) { - ul { - notes.forEach { (title, tags, _, uuid) -> - li("flex justify-between") { - a(classes = "text-blue-200 text-xl hover:underline", href = "/notes/$uuid") { - +title - } - span("space-x-2") { - tags.forEach { - a(href = "?tag=$it", classes = "tag") { - +"#$it" - } - } - } - } - } - } - if (numberOfPages > 1) - pagination(currentPage, numberOfPages, tag) - } else - span { +"No notes yet" } // FIXME if too far in pagination, it it displayed } + if (notes.isNotEmpty()) + noteTable(notes) + else + span { + if (numberOfPages > 1) +"You went too far" + else +"No notes yet" + } + + if (numberOfPages > 1) pagination(currentPage, numberOfPages, tag) } + } + + fun trash( + jwtPayload: JwtPayload, + notes: List, + currentPage: Int, + numberOfPages: Int + ) = renderPage(title = "Notes", jwtPayload = jwtPayload) { + div("container mx-auto p-4") { + div("flex justify-between mb-4") { + h1("text-2xl underline") { +"Deleted notes" } + } + if (notes.isNotEmpty()) + deletedNoteTable(notes) + else + span { + if (numberOfPages > 1) +"You went too far" + else +"No deleted notes" + } + + if (numberOfPages > 1) pagination(currentPage, numberOfPages, null) + } + } + private fun DIV.pagination(currentPage: Int, numberOfPages: Int, tag: String?) { val links = mutableListOf>() diff --git a/app/src/main/kotlin/views/View.kt b/app/src/main/kotlin/views/View.kt index d00d61a..a50e806 100644 --- a/app/src/main/kotlin/views/View.kt +++ b/app/src/main/kotlin/views/View.kt @@ -14,7 +14,7 @@ abstract class View(private val staticFileResolver: StaticFileResolver) { title: String, description: String? = null, jwtPayload: JwtPayload?, - body: BODY.() -> Unit = {}, + body: MAIN.() -> Unit = {}, ) = buildString { appendLine("") appendHTML().html { @@ -29,7 +29,7 @@ abstract class View(private val staticFileResolver: StaticFileResolver) { } body("bg-gray-900 text-white") { navbar(jwtPayload) - main { this@body.body() } + main { body() } } } } diff --git a/app/src/main/kotlin/views/components/DeletedNoteTable.kt b/app/src/main/kotlin/views/components/DeletedNoteTable.kt new file mode 100644 index 0000000..be9300e --- /dev/null +++ b/app/src/main/kotlin/views/components/DeletedNoteTable.kt @@ -0,0 +1,51 @@ +package be.simplenotes.app.views.components + +import be.simplenotes.domain.model.PersistedNoteMetadata +import kotlinx.html.* +import kotlinx.html.ButtonType.submit +import kotlinx.html.FormMethod.post +import kotlinx.html.ThScope.col +import org.http4k.core.Method + +fun FlowContent.deletedNoteTable(notes: List) = div("overflow-x-auto") { + table { + id = "notes" + thead { + tr { + th(col, "w-1/4") { +"Title" } + th(col, "w-1/4") { +"Updated" } + th(col, "w-1/4") { +"Tags" } + th(col, "w-1/4") { +"Restore" } + } + } + tbody { + notes.forEach { (title, tags, updatedAt, uuid) -> + tr { + td { +title } + td("text-center") { +updatedAt.toString() } // TODO: x time ago + td { tags(tags) } + td("text-center") { + form(classes = "inline", method = post, action = "/notes/deleted/$uuid") { + button(classes = "btn btn-red", type = submit, name = "delete") { + +"Delete permanently" + } + button(classes = "ml-2 btn btn-green", type = submit, name = "restore") { + +"Restore" + } + } + } + } + } + } + } +} + +private fun FlowContent.tags(tags: List) { + ul("inline flex flex-wrap justify-center") { + tags.forEach { tag -> + li("mx-2 my-1") { + span("tag") { +"#$tag" } + } + } + } +} diff --git a/app/src/main/kotlin/views/components/Navbar.kt b/app/src/main/kotlin/views/components/Navbar.kt index 8962994..0497926 100644 --- a/app/src/main/kotlin/views/components/Navbar.kt +++ b/app/src/main/kotlin/views/components/Navbar.kt @@ -4,9 +4,14 @@ import be.simplenotes.domain.security.JwtPayload import kotlinx.html.* fun BODY.navbar(jwtPayload: JwtPayload?) { - nav("nav bg-teal-700 shadow-md flex items-center justify-between px-4") { - a(href = "/", classes = "text-2xl text-gray-100 font-bold") { +"SimpleNotes" } + nav { + id = "navbar" + a("/") { + id = "home" + +"SimpleNotes" + } ul("space-x-2") { + id = "navigation" if (jwtPayload != null) { val links = listOf( "/notes" to "Notes", diff --git a/app/src/main/kotlin/views/components/NoteTable.kt b/app/src/main/kotlin/views/components/NoteTable.kt new file mode 100644 index 0000000..32ff42e --- /dev/null +++ b/app/src/main/kotlin/views/components/NoteTable.kt @@ -0,0 +1,39 @@ +package be.simplenotes.app.views.components + +import be.simplenotes.domain.model.PersistedNoteMetadata +import kotlinx.html.* +import kotlinx.html.ThScope.col + +fun FlowContent.noteTable(notes: List) = div("overflow-x-auto") { + table { + id = "notes" + thead { + tr { + th(col, "w-1/2") { +"Title" } + th(col, "w-1/4") { +"Updated" } + th(col, "w-1/4") { +"Tags" } + } + } + tbody { + notes.forEach { (title, tags, updatedAt, uuid) -> + tr { + td { + a(classes = "text-blue-200 font-semibold underline", href = "/notes/$uuid") { +title } + } + td("text-center") { +updatedAt.toString() } // TODO: x time ago + td { tags(tags) } + } + } + } + } +} + +private fun FlowContent.tags(tags: List) { + ul("inline flex flex-wrap justify-center") { + tags.forEach { tag -> + li("mx-2 my-1") { + a(href = "?tag=$tag", classes = "tag") { +"#$tag" } + } + } + } +} diff --git a/css/src/navbar.pcss b/css/src/navbar.pcss new file mode 100644 index 0000000..d975bb6 --- /dev/null +++ b/css/src/navbar.pcss @@ -0,0 +1,33 @@ +#navbar { + height: 96px; + @apply bg-teal-700 shadow-md flex flex-col items-center justify-center px-4; + + #home { + @apply text-2xl text-gray-100 font-bold; + } + + #navigation { + @apply my-2; + } +} + +@screen sm { + #navbar { + height: 64px; + @apply flex-row justify-between; + + #navigation { + @apply my-0; + } + } +} + +.centered { + min-height: calc(100vh - 96px); +} + +@screen sm { + .centered { + min-height: calc(100vh - 64px); + } +} diff --git a/css/src/note-table.pcss b/css/src/note-table.pcss new file mode 100644 index 0000000..5e96f6d --- /dev/null +++ b/css/src/note-table.pcss @@ -0,0 +1,17 @@ +table#notes { + @apply table-auto w-full border-collapse border-2 border-gray-700; + + thead th { + @apply px-4 py-2; + } + + tbody { + tr:nth-child(even) { + @apply bg-gray-800; + } + + td { + @apply border border-gray-700 py-3 px-4; + } + } +} diff --git a/css/src/other.pcss b/css/src/other.pcss index 220da3f..f9a55ea 100644 --- a/css/src/other.pcss +++ b/css/src/other.pcss @@ -1,11 +1,3 @@ -.nav { - height: 64px; -} - -.centered { - min-height: calc(100vh - 64px); -} - .tag { @apply italic font-semibold text-sm bg-teal-500 text-gray-900 rounded-full py-1 px-2 align-middle; diff --git a/css/src/pagination.pcss b/css/src/pagination.pcss index d17cdcf..a8f5d36 100644 --- a/css/src/pagination.pcss +++ b/css/src/pagination.pcss @@ -20,7 +20,7 @@ nav.pages { @apply bg-gray-700; } - &:active { + &.active { @apply bg-teal-800 border-gray-700 text-white; &:hover { diff --git a/css/src/styles.pcss b/css/src/styles.pcss index 35ccd7a..77a2cec 100644 --- a/css/src/styles.pcss +++ b/css/src/styles.pcss @@ -8,6 +8,8 @@ @import "./note.pcss"; @import "./pagination.pcss"; @import "./other.pcss"; +@import "./navbar.pcss"; +@import "./note-table.pcss"; /*noinspection CssUnknownTarget*/ @import "tailwindcss/utilities"; diff --git a/deploy-docker-hub.sh b/deploy-docker-hub.sh index 818c5a9..a4f41b2 100755 --- a/deploy-docker-hub.sh +++ b/deploy-docker-hub.sh @@ -4,5 +4,5 @@ rm app/src/main/resources/css-manifest.json rm app/src/main/resources/static/styles* yarn --cwd css run css-purge \ - && docker build -t hubv/simplenotes . \ - && docker push hubv/simplenotes:latest + && docker build -t hubv/simplenotes:dev . \ + && docker push hubv/simplenotes:dev diff --git a/domain/src/main/kotlin/usecases/NoteService.kt b/domain/src/main/kotlin/usecases/NoteService.kt index 3799c68..937f014 100644 --- a/domain/src/main/kotlin/usecases/NoteService.kt +++ b/domain/src/main/kotlin/usecases/NoteService.kt @@ -29,16 +29,25 @@ class NoteService( .map { Note(it.metadata, markdown = markdownText, html = it.html) } .map { noteRepository.update(userId, uuid, it) } - fun paginatedNotes(userId: Int, page: Int, itemsPerPage: Int = 20, tag: String? = null): PaginatedNotes { - val count = noteRepository.count(userId, tag) + fun paginatedNotes( + userId: Int, + page: Int, + itemsPerPage: Int = 20, + tag: String? = null, + deleted: Boolean = false + ): PaginatedNotes { + val count = noteRepository.count(userId, tag, deleted) val offset = (page - 1) * itemsPerPage val numberOfPages = (count / itemsPerPage) + 1 - val notes = if (count == 0) emptyList() else noteRepository.findAll(userId, itemsPerPage, offset, tag) + val notes = if (count == 0) emptyList() else noteRepository.findAll(userId, itemsPerPage, offset, tag, deleted) return PaginatedNotes(numberOfPages, notes) } fun find(userId: Int, uuid: UUID) = noteRepository.find(userId, uuid) - fun delete(userId: Int, uuid: UUID) = noteRepository.delete(userId, uuid) + fun trash(userId: Int, uuid: UUID) = noteRepository.delete(userId, uuid, permanent = false) + fun restore(userId: Int, uuid: UUID) = noteRepository.restore(userId, uuid) + fun delete(userId: Int, uuid: UUID) = noteRepository.delete(userId, uuid, permanent = true) + } data class PaginatedNotes(val pages: Int, val notes: List) diff --git a/domain/src/main/kotlin/usecases/repositories/NoteRepository.kt b/domain/src/main/kotlin/usecases/repositories/NoteRepository.kt index 6ddefc4..44bf3fe 100644 --- a/domain/src/main/kotlin/usecases/repositories/NoteRepository.kt +++ b/domain/src/main/kotlin/usecases/repositories/NoteRepository.kt @@ -6,12 +6,23 @@ import be.simplenotes.domain.model.PersistedNoteMetadata import java.util.* interface NoteRepository { - fun findAll(userId: Int, limit: Int = 20, offset: Int = 0, tag: String? = null): List + + fun findAll( + userId: Int, + limit: Int = 20, + offset: Int = 0, + tag: String? = null, + deleted: Boolean = false + ): List + + fun count(userId: Int, tag: String? = null, deleted: Boolean = false): Int + fun delete(userId: Int, uuid: UUID, permanent: Boolean = false): Boolean + fun restore(userId: Int, uuid: UUID): Boolean + + // These methods only access notes where `Notes.deleted = false` + fun getTags(userId: Int): List fun exists(userId: Int, uuid: UUID): Boolean fun create(userId: Int, note: Note): PersistedNote fun find(userId: Int, uuid: UUID): PersistedNote? fun update(userId: Int, uuid: UUID, note: Note): PersistedNote? - fun delete(userId: Int, uuid: UUID): Boolean - fun getTags(userId: Int): List - fun count(userId: Int, tag: String? = null): Int } diff --git a/persistance/src/main/kotlin/notes/NoteRepositoryImpl.kt b/persistance/src/main/kotlin/notes/NoteRepositoryImpl.kt index d830947..e50a781 100644 --- a/persistance/src/main/kotlin/notes/NoteRepositoryImpl.kt +++ b/persistance/src/main/kotlin/notes/NoteRepositoryImpl.kt @@ -4,8 +4,7 @@ import be.simplenotes.domain.model.Note import be.simplenotes.domain.model.PersistedNote import be.simplenotes.domain.model.PersistedNoteMetadata import be.simplenotes.domain.usecases.repositories.NoteRepository -import be.simplenotes.persistance.extensions.uuidBinary -import me.liuwj.ktorm.database.* +import me.liuwj.ktorm.database.Database import me.liuwj.ktorm.dsl.* import me.liuwj.ktorm.entity.* import java.time.LocalDateTime @@ -15,7 +14,13 @@ import kotlin.collections.HashMap internal class NoteRepositoryImpl(private val db: Database) : NoteRepository { @Throws(IllegalArgumentException::class) - override fun findAll(userId: Int, limit: Int, offset: Int, tag: String?): List { + override fun findAll( + userId: Int, + limit: Int, + offset: Int, + tag: String?, + deleted: Boolean + ): List { require(limit > 0) { "limit should be positive" } require(offset >= 0) { "offset should not be negative" } @@ -23,13 +28,13 @@ internal class NoteRepositoryImpl(private val db: Database) : NoteRepository { db.from(Tags) .leftJoin(Notes, on = Notes.uuid eq Tags.noteUuid) .select(Notes.uuid) - .where { (Notes.userId eq userId) and (Tags.name eq tag) } + .where { (Notes.userId eq userId) and (Tags.name eq tag) and (Notes.deleted eq deleted) } .map { it[Notes.uuid]!! } } else null var query = db.notes .filterColumns { listOf(it.uuid, it.title, it.updatedAt) } - .filter { it.userId eq userId } + .filter { (it.userId eq userId) and (it.deleted eq deleted) } if (uuids1 != null) query = query.filter { it.uuid inList uuids1 } @@ -55,7 +60,7 @@ internal class NoteRepositoryImpl(private val db: Database) : NoteRepository { } override fun exists(userId: Int, uuid: UUID): Boolean { - return db.notes.any { (it.userId eq userId) and (it.uuid eq uuid) } + return db.notes.any { (it.userId eq userId) and (it.uuid eq uuid) and (it.deleted eq false) } } override fun create(userId: Int, note: Note): PersistedNote { @@ -78,17 +83,17 @@ internal class NoteRepositoryImpl(private val db: Database) : NoteRepository { return entity.toPersistedNote(note.meta.tags) } - @Suppress("UNCHECKED_CAST") override fun find(userId: Int, uuid: UUID): PersistedNote? { val note = db.notes .filterColumns { it.columns - it.userId } .filter { it.uuid eq uuid } - .find { it.userId eq userId } + .find { (it.userId eq userId) and (it.deleted eq false) } ?: return null - val tags = db.tags - .filter { it.noteUuid eq uuid } - .mapColumns { it.name } as List + val tags = db.from(Tags) + .select(Tags.name) + .where { Tags.noteUuid eq uuid } + .map { it[Tags.name]!! } return note.toPersistedNote(tags) } @@ -96,7 +101,7 @@ internal class NoteRepositoryImpl(private val db: Database) : NoteRepository { override fun update(userId: Int, uuid: UUID, note: Note): PersistedNote? { db.useTransaction { val currentNote = db.notes - .find { it.uuid eq uuid and (it.userId eq userId) } + .find { (it.uuid eq uuid) and (it.userId eq userId) and (it.deleted eq false) } ?: return null currentNote.title = note.meta.title @@ -121,20 +126,45 @@ internal class NoteRepositoryImpl(private val db: Database) : NoteRepository { } } - override fun delete(userId: Int, uuid: UUID): Boolean = db.useTransaction { - db.delete(Notes) { it.uuid eq uuid and (it.userId eq userId) } == 1 + override fun delete(userId: Int, uuid: UUID, permanent: Boolean): Boolean { + if (!permanent) { + return db.useTransaction { + db.update(Notes) { + it.deleted to true + it.updatedAt to LocalDateTime.now() + where { it.userId eq userId and (it.uuid eq uuid) } + } + } == 1 + } + + return db.useTransaction { + db.delete(Notes) { it.uuid eq uuid and (it.userId eq userId) } == 1 + } } - @Suppress("UNCHECKED_CAST") - override fun getTags(userId: Int): List { - return db.sequenceOf(Tags) - .filter { it.note.userId eq userId } - .mapColumns(isDistinct = true) { it.name } as List + override fun restore(userId: Int, uuid: UUID): Boolean { + return db.useTransaction { + db.update(Notes) { + it.deleted to false + where { + (it.userId eq userId) and (it.uuid eq uuid) + } + } == 1 + } } - override fun count(userId: Int, tag: String?): Int { - if (tag == null) return db.notes.count { it.userId eq userId } - return db.sequenceOf(Tags) - .count { it.name eq tag and (it.note.userId eq userId) } + override fun getTags(userId: Int): List = + db.from(Tags) + .leftJoin(Notes, on = Notes.uuid eq Tags.noteUuid) + .selectDistinct(Tags.name) + .where { (Notes.userId eq userId) and (Notes.deleted eq false) } + .map { it[Tags.name]!! } + + override fun count(userId: Int, tag: String?, deleted: Boolean): Int { + if (tag == null) return db.notes.count { (it.userId eq userId) and (Notes.deleted eq deleted) } + + return db.sequenceOf(Tags).count { + (it.name eq tag) and (it.note.userId eq userId) and (it.note.deleted eq deleted) + } } } diff --git a/persistance/src/main/kotlin/notes/Notes.kt b/persistance/src/main/kotlin/notes/Notes.kt index 95127fd..2a430f9 100644 --- a/persistance/src/main/kotlin/notes/Notes.kt +++ b/persistance/src/main/kotlin/notes/Notes.kt @@ -24,6 +24,7 @@ internal open class Notes(alias: String?) : Table("Notes", alias) { val html = text("html").bindTo { it.html } val userId = int("user_id").references(Users) { it.user } val updatedAt = datetime("updated_at").bindTo { it.updatedAt } + val deleted = boolean("deleted").bindTo { it.deleted } val user get() = userId.referenceTable as Users } @@ -37,6 +38,7 @@ internal interface NoteEntity : Entity { var markdown: String var html: String var updatedAt: LocalDateTime + var deleted: Boolean var user: UserEntity } @@ -52,6 +54,7 @@ internal fun Note.toEntity(uuid: UUID, userId: Int): NoteEntity { this.markdown = note.markdown this.html = note.html this.uuid = uuid + this.deleted = false this.user["id"] = userId } } diff --git a/persistance/src/main/resources/db/migration/mariadb/V2__Add_deleted_column.sql b/persistance/src/main/resources/db/migration/mariadb/V2__Add_deleted_column.sql new file mode 100644 index 0000000..d704e62 --- /dev/null +++ b/persistance/src/main/resources/db/migration/mariadb/V2__Add_deleted_column.sql @@ -0,0 +1,2 @@ +alter table Notes + add column deleted bool not null default false diff --git a/persistance/src/main/resources/db/migration/other/V2__Add_deleted_column.sql b/persistance/src/main/resources/db/migration/other/V2__Add_deleted_column.sql new file mode 100644 index 0000000..d704e62 --- /dev/null +++ b/persistance/src/main/resources/db/migration/other/V2__Add_deleted_column.sql @@ -0,0 +1,2 @@ +alter table Notes + add column deleted bool not null default false diff --git a/persistance/src/test/kotlin/notes/NoteRepositoryImplTest.kt b/persistance/src/test/kotlin/notes/NoteRepositoryImplTest.kt index 9ba0e03..208722a 100644 --- a/persistance/src/test/kotlin/notes/NoteRepositoryImplTest.kt +++ b/persistance/src/test/kotlin/notes/NoteRepositoryImplTest.kt @@ -224,9 +224,6 @@ internal class NoteRepositoryImplTest { val note = createNote(user1.id, "1", listOf("a", "b")) assertThat(noteRepo.delete(user1.id, note.uuid)) .isTrue - - assertThat(noteRepo.delete(user1.id, note.uuid)) - .isFalse } @Test @@ -289,4 +286,42 @@ internal class NoteRepositoryImplTest { .isEqualToComparingOnlyGivenFields(newNote2, "meta", "markdown", "html") } } + + @Nested + inner class Trash { + + @Test + fun `trashed noted should be restored`() { + val note1 = createNote(user1.id, "1", listOf("a", "b")) + + assertThat(noteRepo.delete(user1.id, note1.uuid, permanent = false)) + .isTrue + + val isDeleted = db.notes + .find { it.uuid eq note1.uuid } + ?.deleted + + assertThat(isDeleted).`as`("Check that Notes.deleted is true").isTrue + + assertThat(noteRepo.restore(user1.id, note1.uuid)).isTrue + + val isDeleted2 = db.notes + .find { it.uuid eq note1.uuid } + ?.deleted + + assertThat(isDeleted2).`as`("Check that Notes.deleted is false after restore()").isFalse + } + + @Test + fun `permanent delete`() { + val note1 = createNote(user1.id, "1", listOf("a", "b")) + assertThat(noteRepo.delete(user1.id, note1.uuid, permanent = true)) + .isTrue + + assertThat(noteRepo.restore(user1.id, note1.uuid)).isFalse + } + + + } + }