diff --git a/modules/backend/src/main/scala/docspell/backend/ops/OItem.scala b/modules/backend/src/main/scala/docspell/backend/ops/OItem.scala index e586bf34..cc2ebfed 100644 --- a/modules/backend/src/main/scala/docspell/backend/ops/OItem.scala +++ b/modules/backend/src/main/scala/docspell/backend/ops/OItem.scala @@ -124,6 +124,8 @@ trait OItem[F[_]] { collective: Ident ): F[AddResult] + def restore(items: NonEmptyList[Ident], collective: Ident): F[UpdateResult] + def setItemDate( item: NonEmptyList[Ident], date: Option[Timestamp], @@ -582,6 +584,17 @@ object OItem { .attempt .map(AddResult.fromUpdate) + def restore( + items: NonEmptyList[Ident], + collective: Ident + ): F[UpdateResult] = + UpdateResult.fromUpdate( + store + .transact( + RItem.restoreStateForCollective(items, ItemState.Created, collective) + ) + ) + def setItemDate( items: NonEmptyList[Ident], date: Option[Timestamp], diff --git a/modules/common/src/main/scala/docspell/common/ItemState.scala b/modules/common/src/main/scala/docspell/common/ItemState.scala index 05149ad7..e59d5049 100644 --- a/modules/common/src/main/scala/docspell/common/ItemState.scala +++ b/modules/common/src/main/scala/docspell/common/ItemState.scala @@ -34,6 +34,7 @@ object ItemState { def processing: ItemState = Processing def created: ItemState = Created def confirmed: ItemState = Confirmed + def deleted: ItemState = Deleted def fromString(str: String): Either[String, ItemState] = str.toLowerCase match { diff --git a/modules/restapi/src/main/resources/docspell-openapi.yml b/modules/restapi/src/main/resources/docspell-openapi.yml index 48a65da4..2256ae44 100644 --- a/modules/restapi/src/main/resources/docspell-openapi.yml +++ b/modules/restapi/src/main/resources/docspell-openapi.yml @@ -1609,7 +1609,9 @@ paths: tags: [ Item ] summary: Delete an item. description: | - Delete an item and all its data permanently. + Delete an item and all its data. This is a "soft delete", the + item is still in the database and can be undeleted. A periodic + job will eventually remove this item from the database. security: - authTokenHeader: [] parameters: @@ -1621,6 +1623,26 @@ paths: application/json: schema: $ref: "#/components/schemas/BasicResult" + /sec/item/{id}/restore: + post: + operationId: "sec-item-restore-by-id" + tags: [ Item ] + summary: Restore a deleted item. + description: | + A deleted item can be restored as long it is still in the + database. This action sets the item state to `created`. + security: + - authTokenHeader: [] + parameters: + - $ref: "#/components/parameters/id" + responses: + 200: + description: Ok + content: + application/json: + schema: + $ref: "#/components/schemas/BasicResult" + /sec/item/{id}/tags: put: operationId: "sec-item-get-tags" @@ -2307,6 +2329,29 @@ paths: schema: $ref: "#/components/schemas/BasicResult" + /sec/items/restoreAll: + post: + operationId: "sec-items-restore-all" + tags: + - Item (Multi Edit) + summary: Restore multiple items. + description: | + Given a list of item ids, restores all of them. + security: + - authTokenHeader: [] + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/IdList" + responses: + 200: + description: Ok + content: + application/json: + schema: + $ref: "#/components/schemas/BasicResult" + /sec/items/tags: post: operationId: "sec-items-add-all-tags" diff --git a/modules/restserver/src/main/scala/docspell/restserver/routes/ItemMultiRoutes.scala b/modules/restserver/src/main/scala/docspell/restserver/routes/ItemMultiRoutes.scala index 2ffadde9..771734ec 100644 --- a/modules/restserver/src/main/scala/docspell/restserver/routes/ItemMultiRoutes.scala +++ b/modules/restserver/src/main/scala/docspell/restserver/routes/ItemMultiRoutes.scala @@ -187,6 +187,14 @@ object ItemMultiRoutes extends MultiIdSupport { resp <- Ok(res) } yield resp + case req @ POST -> Root / "restoreAll" => + for { + json <- req.as[IdList] + items <- readIds[F](json.ids) + res <- backend.item.restore(items, user.account.collective) + resp <- Ok(Conversions.basicResult(res, "Item(s) deleted")) + } yield resp + case req @ PUT -> Root / "customfield" => for { json <- req.as[ItemsAndFieldValue] diff --git a/modules/restserver/src/main/scala/docspell/restserver/routes/ItemRoutes.scala b/modules/restserver/src/main/scala/docspell/restserver/routes/ItemRoutes.scala index 71bdbfb7..9453eb69 100644 --- a/modules/restserver/src/main/scala/docspell/restserver/routes/ItemRoutes.scala +++ b/modules/restserver/src/main/scala/docspell/restserver/routes/ItemRoutes.scala @@ -150,6 +150,12 @@ object ItemRoutes { resp <- Ok(Conversions.basicResult(res, "Item back to created.")) } yield resp + case POST -> Root / Ident(id) / "restore" => + for { + res <- backend.item.restore(NonEmptyList.of(id), user.account.collective) + resp <- Ok(Conversions.basicResult(res, "Item restored.")) + } yield resp + case req @ PUT -> Root / Ident(id) / "tags" => for { tags <- req.as[StringList].map(_.items) diff --git a/modules/store/src/main/scala/docspell/store/records/RItem.scala b/modules/store/src/main/scala/docspell/store/records/RItem.scala index 017e1992..78eb5416 100644 --- a/modules/store/src/main/scala/docspell/store/records/RItem.scala +++ b/modules/store/src/main/scala/docspell/store/records/RItem.scala @@ -152,7 +152,21 @@ object RItem { t <- currentTime n <- DML.update( T, - T.id.in(itemIds) && T.cid === coll, + T.id.in(itemIds) && T.cid === coll && T.state.in(ItemState.validStates), + DML.set(T.state.setTo(itemState), T.updated.setTo(t)) + ) + } yield n + + def restoreStateForCollective( + itemIds: NonEmptyList[Ident], + itemState: ItemState, + coll: Ident + ): ConnectionIO[Int] = + for { + t <- currentTime + n <- DML.update( + T, + T.id.in(itemIds) && T.cid === coll && T.state === ItemState.deleted, DML.set(T.state.setTo(itemState), T.updated.setTo(t)) ) } yield n