diff --git a/app/src/main/kotlin/controllers/NoteController.kt b/app/src/main/kotlin/controllers/NoteController.kt index 5bc2f1c..a755656 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) diff --git a/domain/src/main/kotlin/usecases/NoteService.kt b/domain/src/main/kotlin/usecases/NoteService.kt index 3799c68..5ff2fca 100644 --- a/domain/src/main/kotlin/usecases/NoteService.kt +++ b/domain/src/main/kotlin/usecases/NoteService.kt @@ -38,7 +38,10 @@ class NoteService( } 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..aea00d5 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 (Notes.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 + } + + + } + }