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 c62cc064..fdc68c9d 100644 --- a/modules/backend/src/main/scala/docspell/backend/ops/OItem.scala +++ b/modules/backend/src/main/scala/docspell/backend/ops/OItem.scala @@ -12,6 +12,7 @@ import OItem.{ AttachmentArchiveData, AttachmentData, AttachmentSourceData, + Batch, ItemData, ListItem, Query @@ -24,7 +25,7 @@ trait OItem[F[_]] { def findItem(id: Ident, collective: Ident): F[Option[ItemData]] - def findItems(q: Query, maxResults: Int): F[Vector[ListItem]] + def findItems(q: Query, batch: Batch): F[Vector[ListItem]] def findAttachment(id: Ident, collective: Ident): F[Option[AttachmentData[F]]] @@ -84,6 +85,9 @@ object OItem { type Query = QItem.Query val Query = QItem.Query + type Batch = QItem.Batch + val Batch = QItem.Batch + type ListItem = QItem.ListItem val ListItem = QItem.ListItem @@ -138,8 +142,11 @@ object OItem { .transact(QItem.findItem(id)) .map(opt => opt.flatMap(_.filterCollective(collective))) - def findItems(q: Query, maxResults: Int): F[Vector[ListItem]] = - store.transact(QItem.findItems(q).take(maxResults.toLong)).compile.toVector + def findItems(q: Query, batch: Batch): F[Vector[ListItem]] = + store + .transact(QItem.findItems(q, batch).take(batch.limit.toLong)) + .compile + .toVector def findAttachment(id: Ident, collective: Ident): F[Option[AttachmentData[F]]] = store diff --git a/modules/joex/src/main/scala/docspell/joex/notify/NotifyDueItemsTask.scala b/modules/joex/src/main/scala/docspell/joex/notify/NotifyDueItemsTask.scala index 2b789ff1..055d0d90 100644 --- a/modules/joex/src/main/scala/docspell/joex/notify/NotifyDueItemsTask.scala +++ b/modules/joex/src/main/scala/docspell/joex/notify/NotifyDueItemsTask.scala @@ -8,6 +8,7 @@ import emil.markdown._ import emil.javamail.syntax._ import docspell.common._ +import docspell.backend.ops.OItem.Batch import docspell.store.records._ import docspell.store.queries.QItem import docspell.joex.scheduler.{Context, Task} @@ -15,7 +16,7 @@ import cats.data.OptionT import docspell.joex.mail.EmilHeader object NotifyDueItemsTask { - val maxItems: Long = 7 + val maxItems: Int = 7 type Args = NotifyDueItemsArgs def apply[F[_]: Sync](cfg: MailSendConfig, emil: Emil[F]): Task[F, Args, Unit] = @@ -78,7 +79,11 @@ object NotifyDueItemsTask { dueDateTo = Some(now + Duration.days(ctx.args.remindDays.toLong)), orderAsc = Some(_.dueDate) ) - res <- ctx.store.transact(QItem.findItems(q).take(maxItems)).compile.toVector + res <- + ctx.store + .transact(QItem.findItems(q, Batch.limit(maxItems)).take(maxItems.toLong)) + .compile + .toVector } yield res def makeMail[F[_]: Sync]( diff --git a/modules/restapi/src/main/resources/docspell-openapi.yml b/modules/restapi/src/main/resources/docspell-openapi.yml index e8591141..11c9ba49 100644 --- a/modules/restapi/src/main/resources/docspell-openapi.yml +++ b/modules/restapi/src/main/resources/docspell-openapi.yml @@ -3121,6 +3121,8 @@ components: - tagsInclude - tagsExclude - inbox + - offset + - limit properties: tagsInclude: type: array @@ -3134,6 +3136,16 @@ components: format: ident inbox: type: boolean + offset: + type: integer + format: int32 + limit: + type: integer + format: int32 + description: | + The maximum number of results to return. Note that this + limit is a soft limit, there is some hard limit on the + server, too. direction: type: string format: direction 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 c22d7bca..e49e4178 100644 --- a/modules/restserver/src/main/scala/docspell/restserver/routes/ItemRoutes.scala +++ b/modules/restserver/src/main/scala/docspell/restserver/routes/ItemRoutes.scala @@ -4,6 +4,7 @@ import cats.effect._ import cats.implicits._ import docspell.backend.BackendApp import docspell.backend.auth.AuthToken +import docspell.backend.ops.OItem.Batch import docspell.common.{Ident, ItemState} import org.http4s.HttpRoutes import org.http4s.dsl.Http4sDsl @@ -27,9 +28,12 @@ object ItemRoutes { mask <- req.as[ItemSearch] _ <- logger.ftrace(s"Got search mask: $mask") query = Conversions.mkQuery(mask, user.account.collective) - _ <- logger.ftrace(s"Running query: $query") - items <- backend.item.findItems(query, 100) - resp <- Ok(Conversions.mkItemList(items)) + _ <- logger.ftrace(s"Running query: $query") + items <- backend.item.findItems( + query, + Batch(mask.offset, mask.limit).restrictLimitTo(500) + ) + resp <- Ok(Conversions.mkItemList(items)) } yield resp case GET -> Root / Ident(id) => diff --git a/modules/store/src/main/scala/docspell/store/queries/QItem.scala b/modules/store/src/main/scala/docspell/store/queries/QItem.scala index 5abb2c92..4ce8b7f0 100644 --- a/modules/store/src/main/scala/docspell/store/queries/QItem.scala +++ b/modules/store/src/main/scala/docspell/store/queries/QItem.scala @@ -187,7 +187,22 @@ object QItem { ) } - def findItems(q: Query): Stream[ConnectionIO, ListItem] = { + case class Batch(offset: Int, limit: Int) { + def restrictLimitTo(n: Int): Batch = + Batch(offset, math.min(n, limit)) + } + + object Batch { + val all: Batch = Batch(0, Int.MaxValue) + + def page(n: Int, size: Int): Batch = + Batch(n * size, size) + + def limit(c: Int): Batch = + Batch(0, c) + } + + def findItems(q: Query, batch: Batch): Stream[ConnectionIO, ListItem] = { val IC = RItem.Columns val AC = RAttachment.Columns val PC = RPerson.Columns @@ -202,7 +217,7 @@ object QItem { IC.id.prefix("i").f, IC.name.prefix("i").f, IC.state.prefix("i").f, - coalesce(IC.itemDate.prefix("i").f, IC.created.prefix("i").f), + coalesce(IC.itemDate.prefix("i").f, IC.created.prefix("i").f) ++ fr"i_date", IC.dueDate.prefix("i").f, IC.source.prefix("i").f, IC.incoming.prefix("i").f, @@ -310,11 +325,12 @@ object QItem { case Some(co) => orderBy(coalesce(co(IC).prefix("i").f, IC.created.prefix("i").f) ++ fr"ASC") case None => - orderBy( - coalesce(IC.itemDate.prefix("i").f, IC.created.prefix("i").f) ++ fr"DESC" - ) + orderBy(fr"i_date DESC") } - val frag = query ++ fr"WHERE" ++ cond ++ order + val frag = + query ++ fr"WHERE" ++ cond ++ order ++ (if (batch == Batch.all) Fragment.empty + else + fr"OFFSET ${batch.offset} LIMIT ${batch.limit}") logger.trace(s"List items: $frag") frag.query[ListItem].stream } diff --git a/modules/webapp/src/main/elm/Page/Home/Update.elm b/modules/webapp/src/main/elm/Page/Home/Update.elm index b3e1e327..05c8d850 100644 --- a/modules/webapp/src/main/elm/Page/Home/Update.elm +++ b/modules/webapp/src/main/elm/Page/Home/Update.elm @@ -77,8 +77,11 @@ update key flags msg model = doSearch : Flags -> Model -> ( Model, Cmd Msg ) doSearch flags model = let - mask = + smask = Comp.SearchMenu.getItemSearch model.searchMenuModel + + mask = + { smask | limit = 100 } in ( { model | searchInProgress = True, viewMode = Listing } , Api.itemSearch flags mask ItemSearchResp