Use an enum instead of a boolean to differentiate search

It's not very likely to have more modes of search besides normal and
trashed, but got surprised in that way quite often and it's nicer this
way anyways.
This commit is contained in:
eikek 2021-08-14 15:08:29 +02:00
parent a7b74bd5ae
commit edb344314f
7 changed files with 85 additions and 23 deletions

View File

@ -238,6 +238,10 @@ val openapiScalaSettings = Seq(
field.copy(typeDef = field.copy(typeDef =
TypeDef("EquipmentUse", Imports("docspell.common.EquipmentUse")) TypeDef("EquipmentUse", Imports("docspell.common.EquipmentUse"))
) )
case "searchmode" =>
field =>
field
.copy(typeDef = TypeDef("SearchMode", Imports("docspell.common.SearchMode")))
})) }))
) )

View File

@ -87,11 +87,11 @@ object OSimpleSearch {
useFTS: Boolean, useFTS: Boolean,
resolveDetails: Boolean, resolveDetails: Boolean,
maxNoteLen: Int, maxNoteLen: Int,
deleted: Boolean searchMode: SearchMode
) )
final case class StatsSettings( final case class StatsSettings(
useFTS: Boolean, useFTS: Boolean,
deleted: Boolean searchMode: SearchMode
) )
sealed trait Items { sealed trait Items {
@ -223,8 +223,10 @@ object OSimpleSearch {
// 2. sql+fulltext if fulltextQuery.isDefined && q.nonEmpty && useFTS // 2. sql+fulltext if fulltextQuery.isDefined && q.nonEmpty && useFTS
// 3. sql-only else (if fulltextQuery.isEmpty || !useFTS) // 3. sql-only else (if fulltextQuery.isEmpty || !useFTS)
val validItemQuery = val validItemQuery =
if (settings.deleted) q.withFix(_.andQuery(ItemQuery.Expr.Trashed)) settings.searchMode match {
else q.withFix(_.andQuery(ItemQuery.Expr.ValidItemStates)) case SearchMode.Trashed => q.withFix(_.andQuery(ItemQuery.Expr.Trashed))
case SearchMode.Normal => q.withFix(_.andQuery(ItemQuery.Expr.ValidItemStates))
}
fulltextQuery match { fulltextQuery match {
case Some(ftq) if settings.useFTS => case Some(ftq) if settings.useFTS =>
if (q.isEmpty) { if (q.isEmpty) {
@ -280,8 +282,10 @@ object OSimpleSearch {
settings: StatsSettings settings: StatsSettings
)(q: Query, fulltextQuery: Option[String]): F[SearchSummary] = { )(q: Query, fulltextQuery: Option[String]): F[SearchSummary] = {
val validItemQuery = val validItemQuery =
if (settings.deleted) q.withFix(_.andQuery(ItemQuery.Expr.Trashed)) settings.searchMode match {
else q.withFix(_.andQuery(ItemQuery.Expr.ValidItemStates)) case SearchMode.Trashed => q.withFix(_.andQuery(ItemQuery.Expr.Trashed))
case SearchMode.Normal => q.withFix(_.andQuery(ItemQuery.Expr.ValidItemStates))
}
fulltextQuery match { fulltextQuery match {
case Some(ftq) if settings.useFTS => case Some(ftq) if settings.useFTS =>
if (q.isEmpty) if (q.isEmpty)

View File

@ -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)
}

View File

@ -1478,7 +1478,7 @@ paths:
- $ref: "#/components/parameters/limit" - $ref: "#/components/parameters/limit"
- $ref: "#/components/parameters/offset" - $ref: "#/components/parameters/offset"
- $ref: "#/components/parameters/withDetails" - $ref: "#/components/parameters/withDetails"
- $ref: "#/components/parameters/deleted" - $ref: "#/components/parameters/searchMode"
responses: responses:
200: 200:
description: Ok description: Ok
@ -4114,12 +4114,16 @@ components:
withDetails: withDetails:
type: boolean type: boolean
default: false default: false
deleted: searchMode:
type: boolean type: string
default: false format: searchmode
enum:
- normal
- trashed
default: normal
description: | description: |
If this is true, the search performed only for Specify whether the search query should apply to
soft-deleted items. soft-deleted items or not.
query: query:
type: string type: string
description: | description: |
@ -5846,12 +5850,13 @@ components:
description: Whether to return details to each item. description: Whether to return details to each item.
schema: schema:
type: boolean type: boolean
deleted: searchMode:
name: deleted name: searchMode
in: query in: query
description: Whether to search in soft-deleted items only. description: Whether to search in soft-deleted items only.
schema: schema:
type: boolean type: string
format: searchmode
name: name:
name: name name: name
in: path in: path

View File

@ -7,6 +7,7 @@
package docspell.restserver.http4s package docspell.restserver.http4s
import docspell.common.ContactKind import docspell.common.ContactKind
import docspell.common.SearchMode
import org.http4s.ParseFailure import org.http4s.ParseFailure
import org.http4s.QueryParamDecoder import org.http4s.QueryParamDecoder
@ -23,6 +24,11 @@ object QueryParam {
implicit val queryStringDecoder: QueryParamDecoder[QueryString] = implicit val queryStringDecoder: QueryParamDecoder[QueryString] =
QueryParamDecoder[String].map(s => QueryString(s.trim.toLowerCase)) 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 FullOpt extends OptionalQueryParamDecoderMatcher[Boolean]("full")
object OwningOpt extends OptionalQueryParamDecoderMatcher[Boolean]("owning") object OwningOpt extends OptionalQueryParamDecoderMatcher[Boolean]("owning")
@ -35,7 +41,7 @@ object QueryParam {
object Limit extends OptionalQueryParamDecoderMatcher[Int]("limit") object Limit extends OptionalQueryParamDecoderMatcher[Int]("limit")
object Offset extends OptionalQueryParamDecoderMatcher[Int]("offset") object Offset extends OptionalQueryParamDecoderMatcher[Int]("offset")
object WithDetails extends OptionalQueryParamDecoderMatcher[Boolean]("withDetails") object WithDetails extends OptionalQueryParamDecoderMatcher[Boolean]("withDetails")
object Deleted extends OptionalQueryParamDecoderMatcher[Boolean]("deleted") object SearchKind extends OptionalQueryParamDecoderMatcher[SearchMode]("searchMode")
object WithFallback extends OptionalQueryParamDecoderMatcher[Boolean]("withFallback") object WithFallback extends OptionalQueryParamDecoderMatcher[Boolean]("withFallback")
} }

View File

@ -49,7 +49,7 @@ object ItemRoutes {
HttpRoutes.of { HttpRoutes.of {
case GET -> Root / "search" :? QP.Query(q) :? QP.Limit(limit) :? QP.Offset( case GET -> Root / "search" :? QP.Query(q) :? QP.Limit(limit) :? QP.Offset(
offset offset
) :? QP.WithDetails(detailFlag) :? QP.Deleted(deletedFlag) => ) :? QP.WithDetails(detailFlag) :? QP.SearchKind(searchMode) =>
val batch = Batch(offset.getOrElse(0), limit.getOrElse(cfg.maxItemPageSize)) val batch = Batch(offset.getOrElse(0), limit.getOrElse(cfg.maxItemPageSize))
.restrictLimitTo(cfg.maxItemPageSize) .restrictLimitTo(cfg.maxItemPageSize)
val itemQuery = ItemQueryString(q) val itemQuery = ItemQueryString(q)
@ -58,17 +58,17 @@ object ItemRoutes {
cfg.fullTextSearch.enabled, cfg.fullTextSearch.enabled,
detailFlag.getOrElse(false), detailFlag.getOrElse(false),
cfg.maxNoteLength, cfg.maxNoteLength,
deletedFlag.getOrElse(false) searchMode.getOrElse(SearchMode.Normal)
) )
val fixQuery = Query.Fix(user.account, None, None) val fixQuery = Query.Fix(user.account, None, None)
searchItems(backend, dsl)(settings, fixQuery, itemQuery) 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 itemQuery = ItemQueryString(q)
val fixQuery = Query.Fix(user.account, None, None) val fixQuery = Query.Fix(user.account, None, None)
val settings = OSimpleSearch.StatsSettings( val settings = OSimpleSearch.StatsSettings(
useFTS = cfg.fullTextSearch.enabled, useFTS = cfg.fullTextSearch.enabled,
deleted = deletedFlag.getOrElse(false) searchMode = searchMode.getOrElse(SearchMode.Normal)
) )
searchItemStats(backend, dsl)(settings, fixQuery, itemQuery) searchItemStats(backend, dsl)(settings, fixQuery, itemQuery)
@ -87,7 +87,7 @@ object ItemRoutes {
cfg.fullTextSearch.enabled, cfg.fullTextSearch.enabled,
userQuery.withDetails.getOrElse(false), userQuery.withDetails.getOrElse(false),
cfg.maxNoteLength, cfg.maxNoteLength,
deleted = userQuery.deleted.getOrElse(false) searchMode = userQuery.searchMode.getOrElse(SearchMode.Normal)
) )
fixQuery = Query.Fix(user.account, None, None) fixQuery = Query.Fix(user.account, None, None)
resp <- searchItems(backend, dsl)(settings, fixQuery, itemQuery) resp <- searchItems(backend, dsl)(settings, fixQuery, itemQuery)
@ -100,7 +100,7 @@ object ItemRoutes {
fixQuery = Query.Fix(user.account, None, None) fixQuery = Query.Fix(user.account, None, None)
settings = OSimpleSearch.StatsSettings( settings = OSimpleSearch.StatsSettings(
useFTS = cfg.fullTextSearch.enabled, useFTS = cfg.fullTextSearch.enabled,
deleted = userQuery.deleted.getOrElse(false) searchMode = userQuery.searchMode.getOrElse(SearchMode.Normal)
) )
resp <- searchItemStats(backend, dsl)(settings, fixQuery, itemQuery) resp <- searchItemStats(backend, dsl)(settings, fixQuery, itemQuery)
} yield resp } yield resp

View File

@ -79,7 +79,7 @@ request mq =
, limit = Nothing , limit = Nothing
, withDetails = Just True , withDetails = Just True
, query = renderMaybe mq , query = renderMaybe mq
, deleted = Just False , searchMode = Nothing
} }