From a7b74bd5aef65ba5bb517cba50b80ed99e924d8c Mon Sep 17 00:00:00 2001 From: eikek Date: Sat, 14 Aug 2021 14:53:05 +0200 Subject: [PATCH] Allow to search in soft-deleted items A new query/request parameter can be used to apply a search to only soft-deleted items. The query expression `Trashed` has been introduced which selects only items with state `Deleted`. This is another option an analog to `ValidItemStates` (both cannot be used together as they would select no items). This new query node is not added to the parser, because users may not use it in their own queries - it must be part of the "fixed" query so the application can control in which subset to search (it would otherwise be possible to select any items). --- .../docspell/backend/ops/OSimpleSearch.scala | 25 +++++++++----- .../main/scala/docspell/query/ItemQuery.scala | 3 +- .../docspell/query/internal/ExprUtil.scala | 3 +- .../src/main/resources/docspell-openapi.yml | 14 ++++++++ .../restserver/http4s/QueryParam.scala | 1 + .../restserver/routes/ItemRoutes.scala | 33 ++++++++++++------- .../qb/generator/ItemQueryGenerator.scala | 3 ++ .../webapp/src/main/elm/Data/ItemQuery.elm | 1 + 8 files changed, 62 insertions(+), 21 deletions(-) diff --git a/modules/backend/src/main/scala/docspell/backend/ops/OSimpleSearch.scala b/modules/backend/src/main/scala/docspell/backend/ops/OSimpleSearch.scala index 9050bb06..aa2ecf00 100644 --- a/modules/backend/src/main/scala/docspell/backend/ops/OSimpleSearch.scala +++ b/modules/backend/src/main/scala/docspell/backend/ops/OSimpleSearch.scala @@ -39,7 +39,7 @@ trait OSimpleSearch[F[_]] { * and not the results. */ def searchSummary( - useFTS: Boolean + settings: StatsSettings )(q: Query, fulltextQuery: Option[String]): F[SearchSummary] /** Calls `search` by parsing the given query string into a query that @@ -56,12 +56,12 @@ trait OSimpleSearch[F[_]] { * results. */ final def searchSummaryByString( - useFTS: Boolean + settings: StatsSettings )(fix: Query.Fix, q: ItemQueryString)(implicit F: Applicative[F] ): F[StringSearchResult[SearchSummary]] = OSimpleSearch.applySearch[F, SearchSummary](fix, q)((iq, fts) => - searchSummary(useFTS)(iq, fts) + searchSummary(settings)(iq, fts) ) } @@ -86,7 +86,12 @@ object OSimpleSearch { batch: Batch, useFTS: Boolean, resolveDetails: Boolean, - maxNoteLen: Int + maxNoteLen: Int, + deleted: Boolean + ) + final case class StatsSettings( + useFTS: Boolean, + deleted: Boolean ) sealed trait Items { @@ -217,7 +222,9 @@ object OSimpleSearch { // 1. fulltext only if fulltextQuery.isDefined && q.isEmpty && useFTS // 2. sql+fulltext if fulltextQuery.isDefined && q.nonEmpty && useFTS // 3. sql-only else (if fulltextQuery.isEmpty || !useFTS) - val validItemQuery = q.withFix(_.andQuery(ItemQuery.Expr.ValidItemStates)) + val validItemQuery = + if (settings.deleted) q.withFix(_.andQuery(ItemQuery.Expr.Trashed)) + else q.withFix(_.andQuery(ItemQuery.Expr.ValidItemStates)) fulltextQuery match { case Some(ftq) if settings.useFTS => if (q.isEmpty) { @@ -270,11 +277,13 @@ object OSimpleSearch { } def searchSummary( - useFTS: Boolean + settings: StatsSettings )(q: Query, fulltextQuery: Option[String]): F[SearchSummary] = { - val validItemQuery = q.withFix(_.andQuery(ItemQuery.Expr.ValidItemStates)) + val validItemQuery = + if (settings.deleted) q.withFix(_.andQuery(ItemQuery.Expr.Trashed)) + else q.withFix(_.andQuery(ItemQuery.Expr.ValidItemStates)) fulltextQuery match { - case Some(ftq) if useFTS => + case Some(ftq) if settings.useFTS => if (q.isEmpty) fts.findIndexOnlySummary(q.fix.account, OFulltext.FtsInput(ftq)) else diff --git a/modules/query/shared/src/main/scala/docspell/query/ItemQuery.scala b/modules/query/shared/src/main/scala/docspell/query/ItemQuery.scala index 082b6f44..b2a78aa3 100644 --- a/modules/query/shared/src/main/scala/docspell/query/ItemQuery.scala +++ b/modules/query/shared/src/main/scala/docspell/query/ItemQuery.scala @@ -125,7 +125,8 @@ object ItemQuery { final case class ChecksumMatch(checksum: String) extends Expr final case class AttachId(id: String) extends Expr - case object ValidItemStates extends Expr + final case object ValidItemStates extends Expr + final case object Trashed extends Expr // things that can be expressed with terms above sealed trait MacroExpr extends Expr { diff --git a/modules/query/shared/src/main/scala/docspell/query/internal/ExprUtil.scala b/modules/query/shared/src/main/scala/docspell/query/internal/ExprUtil.scala index 9c25edcf..8b15df94 100644 --- a/modules/query/shared/src/main/scala/docspell/query/internal/ExprUtil.scala +++ b/modules/query/shared/src/main/scala/docspell/query/internal/ExprUtil.scala @@ -75,9 +75,10 @@ object ExprUtil { expr case AttachId(_) => expr - case ValidItemStates => expr + case Trashed => + expr } private def spliceAnd(nodes: Nel[Expr]): Nel[Expr] = diff --git a/modules/restapi/src/main/resources/docspell-openapi.yml b/modules/restapi/src/main/resources/docspell-openapi.yml index f2d8d1d9..a45d266f 100644 --- a/modules/restapi/src/main/resources/docspell-openapi.yml +++ b/modules/restapi/src/main/resources/docspell-openapi.yml @@ -1478,6 +1478,7 @@ paths: - $ref: "#/components/parameters/limit" - $ref: "#/components/parameters/offset" - $ref: "#/components/parameters/withDetails" + - $ref: "#/components/parameters/deleted" responses: 200: description: Ok @@ -1576,6 +1577,7 @@ paths: - authTokenHeader: [] parameters: - $ref: "#/components/parameters/q" + - $ref: "#/components/parameters/deleted" responses: 200: description: Ok @@ -4112,6 +4114,12 @@ components: withDetails: type: boolean default: false + deleted: + type: boolean + default: false + description: | + If this is true, the search performed only for + soft-deleted items. query: type: string description: | @@ -5838,6 +5846,12 @@ components: description: Whether to return details to each item. schema: type: boolean + deleted: + name: deleted + in: query + description: Whether to search in soft-deleted items only. + schema: + type: boolean name: name: name in: path diff --git a/modules/restserver/src/main/scala/docspell/restserver/http4s/QueryParam.scala b/modules/restserver/src/main/scala/docspell/restserver/http4s/QueryParam.scala index 74c2b418..da9afe80 100644 --- a/modules/restserver/src/main/scala/docspell/restserver/http4s/QueryParam.scala +++ b/modules/restserver/src/main/scala/docspell/restserver/http4s/QueryParam.scala @@ -35,6 +35,7 @@ object QueryParam { object Limit extends OptionalQueryParamDecoderMatcher[Int]("limit") object Offset extends OptionalQueryParamDecoderMatcher[Int]("offset") object WithDetails extends OptionalQueryParamDecoderMatcher[Boolean]("withDetails") + object Deleted extends OptionalQueryParamDecoderMatcher[Boolean]("deleted") object WithFallback extends OptionalQueryParamDecoderMatcher[Boolean]("withFallback") } 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 803fc975..02030d7d 100644 --- a/modules/restserver/src/main/scala/docspell/restserver/routes/ItemRoutes.scala +++ b/modules/restserver/src/main/scala/docspell/restserver/routes/ItemRoutes.scala @@ -49,7 +49,7 @@ object ItemRoutes { HttpRoutes.of { case GET -> Root / "search" :? QP.Query(q) :? QP.Limit(limit) :? QP.Offset( offset - ) :? QP.WithDetails(detailFlag) => + ) :? QP.WithDetails(detailFlag) :? QP.Deleted(deletedFlag) => val batch = Batch(offset.getOrElse(0), limit.getOrElse(cfg.maxItemPageSize)) .restrictLimitTo(cfg.maxItemPageSize) val itemQuery = ItemQueryString(q) @@ -57,15 +57,20 @@ object ItemRoutes { batch, cfg.fullTextSearch.enabled, detailFlag.getOrElse(false), - cfg.maxNoteLength + cfg.maxNoteLength, + deletedFlag.getOrElse(false) ) val fixQuery = Query.Fix(user.account, None, None) searchItems(backend, dsl)(settings, fixQuery, itemQuery) - case GET -> Root / "searchStats" :? QP.Query(q) => + case GET -> Root / "searchStats" :? QP.Query(q) :? QP.Deleted(deletedFlag) => val itemQuery = ItemQueryString(q) val fixQuery = Query.Fix(user.account, None, None) - searchItemStats(backend, dsl)(cfg.fullTextSearch.enabled, fixQuery, itemQuery) + val settings = OSimpleSearch.StatsSettings( + useFTS = cfg.fullTextSearch.enabled, + deleted = deletedFlag.getOrElse(false) + ) + searchItemStats(backend, dsl)(settings, fixQuery, itemQuery) case req @ POST -> Root / "search" => for { @@ -81,7 +86,8 @@ object ItemRoutes { batch, cfg.fullTextSearch.enabled, userQuery.withDetails.getOrElse(false), - cfg.maxNoteLength + cfg.maxNoteLength, + deleted = userQuery.deleted.getOrElse(false) ) fixQuery = Query.Fix(user.account, None, None) resp <- searchItems(backend, dsl)(settings, fixQuery, itemQuery) @@ -92,11 +98,11 @@ object ItemRoutes { userQuery <- req.as[ItemQuery] itemQuery = ItemQueryString(userQuery.query) fixQuery = Query.Fix(user.account, None, None) - resp <- searchItemStats(backend, dsl)( - cfg.fullTextSearch.enabled, - fixQuery, - itemQuery + settings = OSimpleSearch.StatsSettings( + useFTS = cfg.fullTextSearch.enabled, + deleted = userQuery.deleted.getOrElse(false) ) + resp <- searchItemStats(backend, dsl)(settings, fixQuery, itemQuery) } yield resp case req @ POST -> Root / "searchIndex" => @@ -443,10 +449,15 @@ object ItemRoutes { private def searchItemStats[F[_]: Sync]( backend: BackendApp[F], dsl: Http4sDsl[F] - )(ftsEnabled: Boolean, fixQuery: Query.Fix, itemQuery: ItemQueryString) = { + )( + settings: OSimpleSearch.StatsSettings, + fixQuery: Query.Fix, + itemQuery: ItemQueryString + ) = { import dsl._ + backend.simpleSearch - .searchSummaryByString(ftsEnabled)(fixQuery, itemQuery) + .searchSummaryByString(settings)(fixQuery, itemQuery) .flatMap { case StringSearchResult.Success(summary) => Ok(Conversions.mkSearchStats(summary)) diff --git a/modules/store/src/main/scala/docspell/store/qb/generator/ItemQueryGenerator.scala b/modules/store/src/main/scala/docspell/store/qb/generator/ItemQueryGenerator.scala index 24fed950..10bf7dec 100644 --- a/modules/store/src/main/scala/docspell/store/qb/generator/ItemQueryGenerator.scala +++ b/modules/store/src/main/scala/docspell/store/qb/generator/ItemQueryGenerator.scala @@ -126,6 +126,9 @@ object ItemQueryGenerator { case Expr.ValidItemStates => tables.item.state.in(ItemState.validStates) + case Expr.Trashed => + tables.item.state === ItemState.Deleted + case Expr.TagIdsMatch(op, tags) => val ids = tags.toList.flatMap(s => Ident.fromString(s).toOption) Nel diff --git a/modules/webapp/src/main/elm/Data/ItemQuery.elm b/modules/webapp/src/main/elm/Data/ItemQuery.elm index 72ab54ca..02ceb825 100644 --- a/modules/webapp/src/main/elm/Data/ItemQuery.elm +++ b/modules/webapp/src/main/elm/Data/ItemQuery.elm @@ -79,6 +79,7 @@ request mq = , limit = Nothing , withDetails = Just True , query = renderMaybe mq + , deleted = Just False }