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 }