diff --git a/build.sbt b/build.sbt index 3eea7c5e..3d1b1272 100644 --- a/build.sbt +++ b/build.sbt @@ -238,6 +238,10 @@ val openapiScalaSettings = Seq( field.copy(typeDef = TypeDef("EquipmentUse", Imports("docspell.common.EquipmentUse")) ) + case "searchmode" => + field => + field + .copy(typeDef = TypeDef("SearchMode", Imports("docspell.common.SearchMode"))) })) ) 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 aa2ecf00..2ae4cd14 100644 --- a/modules/backend/src/main/scala/docspell/backend/ops/OSimpleSearch.scala +++ b/modules/backend/src/main/scala/docspell/backend/ops/OSimpleSearch.scala @@ -87,11 +87,11 @@ object OSimpleSearch { useFTS: Boolean, resolveDetails: Boolean, maxNoteLen: Int, - deleted: Boolean + searchMode: SearchMode ) final case class StatsSettings( useFTS: Boolean, - deleted: Boolean + searchMode: SearchMode ) sealed trait Items { @@ -223,8 +223,10 @@ object OSimpleSearch { // 2. sql+fulltext if fulltextQuery.isDefined && q.nonEmpty && useFTS // 3. sql-only else (if fulltextQuery.isEmpty || !useFTS) val validItemQuery = - if (settings.deleted) q.withFix(_.andQuery(ItemQuery.Expr.Trashed)) - else q.withFix(_.andQuery(ItemQuery.Expr.ValidItemStates)) + settings.searchMode match { + case SearchMode.Trashed => q.withFix(_.andQuery(ItemQuery.Expr.Trashed)) + case SearchMode.Normal => q.withFix(_.andQuery(ItemQuery.Expr.ValidItemStates)) + } fulltextQuery match { case Some(ftq) if settings.useFTS => if (q.isEmpty) { @@ -280,8 +282,10 @@ object OSimpleSearch { settings: StatsSettings )(q: Query, fulltextQuery: Option[String]): F[SearchSummary] = { val validItemQuery = - if (settings.deleted) q.withFix(_.andQuery(ItemQuery.Expr.Trashed)) - else q.withFix(_.andQuery(ItemQuery.Expr.ValidItemStates)) + settings.searchMode match { + case SearchMode.Trashed => q.withFix(_.andQuery(ItemQuery.Expr.Trashed)) + case SearchMode.Normal => q.withFix(_.andQuery(ItemQuery.Expr.ValidItemStates)) + } fulltextQuery match { case Some(ftq) if settings.useFTS => if (q.isEmpty) diff --git a/modules/common/src/main/scala/docspell/common/SearchMode.scala b/modules/common/src/main/scala/docspell/common/SearchMode.scala new file mode 100644 index 00000000..451f5de9 --- /dev/null +++ b/modules/common/src/main/scala/docspell/common/SearchMode.scala @@ -0,0 +1,43 @@ +/* + * Copyright 2020 Docspell Contributors + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package docspell.common + +import cats.data.NonEmptyList + +import io.circe.Decoder +import io.circe.Encoder + +sealed trait SearchMode { self: Product => + + final def name: String = + productPrefix.toLowerCase + +} + +object SearchMode { + + final case object Normal extends SearchMode + final case object Trashed extends SearchMode + + def fromString(str: String): Either[String, SearchMode] = + str.toLowerCase match { + case "normal" => Right(Normal) + case "trashed" => Right(Trashed) + case _ => Left(s"Invalid search mode: $str") + } + + val all: NonEmptyList[SearchMode] = + NonEmptyList.of(Normal, Trashed) + + def unsafe(str: String): SearchMode = + fromString(str).fold(sys.error, identity) + + implicit val jsonDecoder: Decoder[SearchMode] = + Decoder.decodeString.emap(fromString) + implicit val jsonEncoder: Encoder[SearchMode] = + Encoder.encodeString.contramap(_.name) +} diff --git a/modules/restapi/src/main/resources/docspell-openapi.yml b/modules/restapi/src/main/resources/docspell-openapi.yml index a45d266f..48a65da4 100644 --- a/modules/restapi/src/main/resources/docspell-openapi.yml +++ b/modules/restapi/src/main/resources/docspell-openapi.yml @@ -1478,7 +1478,7 @@ paths: - $ref: "#/components/parameters/limit" - $ref: "#/components/parameters/offset" - $ref: "#/components/parameters/withDetails" - - $ref: "#/components/parameters/deleted" + - $ref: "#/components/parameters/searchMode" responses: 200: description: Ok @@ -4114,12 +4114,16 @@ components: withDetails: type: boolean default: false - deleted: - type: boolean - default: false + searchMode: + type: string + format: searchmode + enum: + - normal + - trashed + default: normal description: | - If this is true, the search performed only for - soft-deleted items. + Specify whether the search query should apply to + soft-deleted items or not. query: type: string description: | @@ -5846,12 +5850,13 @@ components: description: Whether to return details to each item. schema: type: boolean - deleted: - name: deleted + searchMode: + name: searchMode in: query description: Whether to search in soft-deleted items only. schema: - type: boolean + type: string + format: searchmode 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 da9afe80..23619cd3 100644 --- a/modules/restserver/src/main/scala/docspell/restserver/http4s/QueryParam.scala +++ b/modules/restserver/src/main/scala/docspell/restserver/http4s/QueryParam.scala @@ -7,6 +7,7 @@ package docspell.restserver.http4s import docspell.common.ContactKind +import docspell.common.SearchMode import org.http4s.ParseFailure import org.http4s.QueryParamDecoder @@ -23,6 +24,11 @@ object QueryParam { implicit val queryStringDecoder: QueryParamDecoder[QueryString] = QueryParamDecoder[String].map(s => QueryString(s.trim.toLowerCase)) + implicit val searchModeDecoder: QueryParamDecoder[SearchMode] = + QueryParamDecoder[String].emap(str => + SearchMode.fromString(str).left.map(s => ParseFailure(str, s)) + ) + object FullOpt extends OptionalQueryParamDecoderMatcher[Boolean]("full") object OwningOpt extends OptionalQueryParamDecoderMatcher[Boolean]("owning") @@ -35,7 +41,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 SearchKind extends OptionalQueryParamDecoderMatcher[SearchMode]("searchMode") 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 02030d7d..71bdbfb7 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.Deleted(deletedFlag) => + ) :? QP.WithDetails(detailFlag) :? QP.SearchKind(searchMode) => val batch = Batch(offset.getOrElse(0), limit.getOrElse(cfg.maxItemPageSize)) .restrictLimitTo(cfg.maxItemPageSize) val itemQuery = ItemQueryString(q) @@ -58,17 +58,17 @@ object ItemRoutes { cfg.fullTextSearch.enabled, detailFlag.getOrElse(false), cfg.maxNoteLength, - deletedFlag.getOrElse(false) + searchMode.getOrElse(SearchMode.Normal) ) val fixQuery = Query.Fix(user.account, None, None) searchItems(backend, dsl)(settings, fixQuery, itemQuery) - case GET -> Root / "searchStats" :? QP.Query(q) :? QP.Deleted(deletedFlag) => + case GET -> Root / "searchStats" :? QP.Query(q) :? QP.SearchKind(searchMode) => val itemQuery = ItemQueryString(q) val fixQuery = Query.Fix(user.account, None, None) val settings = OSimpleSearch.StatsSettings( useFTS = cfg.fullTextSearch.enabled, - deleted = deletedFlag.getOrElse(false) + searchMode = searchMode.getOrElse(SearchMode.Normal) ) searchItemStats(backend, dsl)(settings, fixQuery, itemQuery) @@ -87,7 +87,7 @@ object ItemRoutes { cfg.fullTextSearch.enabled, userQuery.withDetails.getOrElse(false), cfg.maxNoteLength, - deleted = userQuery.deleted.getOrElse(false) + searchMode = userQuery.searchMode.getOrElse(SearchMode.Normal) ) fixQuery = Query.Fix(user.account, None, None) resp <- searchItems(backend, dsl)(settings, fixQuery, itemQuery) @@ -100,7 +100,7 @@ object ItemRoutes { fixQuery = Query.Fix(user.account, None, None) settings = OSimpleSearch.StatsSettings( useFTS = cfg.fullTextSearch.enabled, - deleted = userQuery.deleted.getOrElse(false) + searchMode = userQuery.searchMode.getOrElse(SearchMode.Normal) ) resp <- searchItemStats(backend, dsl)(settings, fixQuery, itemQuery) } yield resp diff --git a/modules/webapp/src/main/elm/Data/ItemQuery.elm b/modules/webapp/src/main/elm/Data/ItemQuery.elm index 02ceb825..46d1e4be 100644 --- a/modules/webapp/src/main/elm/Data/ItemQuery.elm +++ b/modules/webapp/src/main/elm/Data/ItemQuery.elm @@ -79,7 +79,7 @@ request mq = , limit = Nothing , withDetails = Just True , query = renderMaybe mq - , deleted = Just False + , searchMode = Nothing }