From 8814de3c38c95157c5b0ca1c3c827afdb269cd14 Mon Sep 17 00:00:00 2001 From: Eike Kettner <eike.kettner@posteo.de> Date: Thu, 2 Jan 2020 19:59:46 +0100 Subject: [PATCH] Allow simple search when listing meta data --- .../docspell/backend/ops/OEquipment.scala | 6 +- .../docspell/backend/ops/OOrganization.scala | 24 +++--- .../scala/docspell/backend/ops/OTag.scala | 6 +- .../main/scala/docspell/common/Ident.scala | 9 ++- .../src/main/resources/docspell-openapi.yml | 13 ++++ .../restserver/routes/EquipmentRoutes.scala | 5 +- .../routes/OrganizationRoutes.scala | 7 +- .../restserver/routes/PersonRoutes.scala | 7 +- .../restserver/routes/TagRoutes.scala | 5 +- .../store/queries/QOrganization.scala | 73 +++++++++++++++++-- .../docspell/store/records/REquipment.scala | 12 ++- .../store/records/ROrganization.scala | 15 +++- .../docspell/store/records/RPerson.scala | 15 +++- .../scala/docspell/store/records/RTag.scala | 12 ++- 14 files changed, 163 insertions(+), 46 deletions(-) diff --git a/modules/backend/src/main/scala/docspell/backend/ops/OEquipment.scala b/modules/backend/src/main/scala/docspell/backend/ops/OEquipment.scala index 279b8db3..5f6dc1ca 100644 --- a/modules/backend/src/main/scala/docspell/backend/ops/OEquipment.scala +++ b/modules/backend/src/main/scala/docspell/backend/ops/OEquipment.scala @@ -8,7 +8,7 @@ import docspell.store.records.{REquipment, RItem} trait OEquipment[F[_]] { - def findAll(account: AccountId): F[Vector[REquipment]] + def findAll(account: AccountId, nameQuery: Option[String]): F[Vector[REquipment]] def add(s: REquipment): F[AddResult] @@ -21,8 +21,8 @@ object OEquipment { def apply[F[_]: Effect](store: Store[F]): Resource[F, OEquipment[F]] = Resource.pure(new OEquipment[F] { - def findAll(account: AccountId): F[Vector[REquipment]] = - store.transact(REquipment.findAll(account.collective, _.name)) + def findAll(account: AccountId, nameQuery: Option[String]): F[Vector[REquipment]] = + store.transact(REquipment.findAll(account.collective, nameQuery, _.name)) def add(e: REquipment): F[AddResult] = { def insert = REquipment.insert(e) diff --git a/modules/backend/src/main/scala/docspell/backend/ops/OOrganization.scala b/modules/backend/src/main/scala/docspell/backend/ops/OOrganization.scala index 5bf7cd11..a00b7afd 100644 --- a/modules/backend/src/main/scala/docspell/backend/ops/OOrganization.scala +++ b/modules/backend/src/main/scala/docspell/backend/ops/OOrganization.scala @@ -9,17 +9,17 @@ import OOrganization._ import docspell.store.queries.QOrganization trait OOrganization[F[_]] { - def findAllOrg(account: AccountId): F[Vector[OrgAndContacts]] + def findAllOrg(account: AccountId, query: Option[String]): F[Vector[OrgAndContacts]] - def findAllOrgRefs(account: AccountId): F[Vector[IdRef]] + def findAllOrgRefs(account: AccountId, nameQuery: Option[String]): F[Vector[IdRef]] def addOrg(s: OrgAndContacts): F[AddResult] def updateOrg(s: OrgAndContacts): F[AddResult] - def findAllPerson(account: AccountId): F[Vector[PersonAndContacts]] + def findAllPerson(account: AccountId, query: Option[String]): F[Vector[PersonAndContacts]] - def findAllPersonRefs(account: AccountId): F[Vector[IdRef]] + def findAllPersonRefs(account: AccountId, nameQuery: Option[String]): F[Vector[IdRef]] def addPerson(s: PersonAndContacts): F[AddResult] @@ -39,15 +39,15 @@ object OOrganization { def apply[F[_]: Effect](store: Store[F]): Resource[F, OOrganization[F]] = Resource.pure(new OOrganization[F] { - def findAllOrg(account: AccountId): F[Vector[OrgAndContacts]] = + def findAllOrg(account: AccountId, query: Option[String]): F[Vector[OrgAndContacts]] = store - .transact(QOrganization.findOrgAndContact(account.collective, _.name)) + .transact(QOrganization.findOrgAndContact(account.collective, query, _.name)) .map({ case (org, cont) => OrgAndContacts(org, cont) }) .compile .toVector - def findAllOrgRefs(account: AccountId): F[Vector[IdRef]] = - store.transact(ROrganization.findAllRef(account.collective, _.name)) + def findAllOrgRefs(account: AccountId, nameQuery: Option[String]): F[Vector[IdRef]] = + store.transact(ROrganization.findAllRef(account.collective, nameQuery, _.name)) def addOrg(s: OrgAndContacts): F[AddResult] = QOrganization.addOrg(s.org, s.contacts, s.org.cid)(store) @@ -55,15 +55,15 @@ object OOrganization { def updateOrg(s: OrgAndContacts): F[AddResult] = QOrganization.updateOrg(s.org, s.contacts, s.org.cid)(store) - def findAllPerson(account: AccountId): F[Vector[PersonAndContacts]] = + def findAllPerson(account: AccountId, query: Option[String]): F[Vector[PersonAndContacts]] = store - .transact(QOrganization.findPersonAndContact(account.collective, _.name)) + .transact(QOrganization.findPersonAndContact(account.collective, query, _.name)) .map({ case (person, cont) => PersonAndContacts(person, cont) }) .compile .toVector - def findAllPersonRefs(account: AccountId): F[Vector[IdRef]] = - store.transact(RPerson.findAllRef(account.collective, _.name)) + def findAllPersonRefs(account: AccountId, nameQuery: Option[String]): F[Vector[IdRef]] = + store.transact(RPerson.findAllRef(account.collective, nameQuery, _.name)) def addPerson(s: PersonAndContacts): F[AddResult] = QOrganization.addPerson(s.person, s.contacts, s.person.cid)(store) diff --git a/modules/backend/src/main/scala/docspell/backend/ops/OTag.scala b/modules/backend/src/main/scala/docspell/backend/ops/OTag.scala index 1414c3fc..29a9748c 100644 --- a/modules/backend/src/main/scala/docspell/backend/ops/OTag.scala +++ b/modules/backend/src/main/scala/docspell/backend/ops/OTag.scala @@ -8,7 +8,7 @@ import docspell.store.records.{RTag, RTagItem} trait OTag[F[_]] { - def findAll(account: AccountId): F[Vector[RTag]] + def findAll(account: AccountId, nameQuery: Option[String]): F[Vector[RTag]] def add(s: RTag): F[AddResult] @@ -21,8 +21,8 @@ object OTag { def apply[F[_]: Effect](store: Store[F]): Resource[F, OTag[F]] = Resource.pure(new OTag[F] { - def findAll(account: AccountId): F[Vector[RTag]] = - store.transact(RTag.findAll(account.collective, _.name)) + def findAll(account: AccountId, nameQuery: Option[String]): F[Vector[RTag]] = + store.transact(RTag.findAll(account.collective, nameQuery, _.name)) def add(t: RTag): F[AddResult] = { def insert = RTag.insert(t) diff --git a/modules/common/src/main/scala/docspell/common/Ident.scala b/modules/common/src/main/scala/docspell/common/Ident.scala index 11cee38d..199bd225 100644 --- a/modules/common/src/main/scala/docspell/common/Ident.scala +++ b/modules/common/src/main/scala/docspell/common/Ident.scala @@ -3,14 +3,17 @@ package docspell.common import java.security.SecureRandom import java.util.UUID +import cats.Eq +import cats.implicits._ import cats.effect.Sync import io.circe.{Decoder, Encoder} import scodec.bits.ByteVector -case class Ident(id: String) { -} +case class Ident(id: String) {} object Ident { + implicit val identEq: Eq[Ident] = + Eq.by(_.id) val chars: Set[Char] = (('A' to 'Z') ++ ('a' to 'z') ++ ('0' to '9') ++ "-_").toSet @@ -46,8 +49,6 @@ object Ident { def unapply(arg: String): Option[Ident] = fromString(arg).toOption - - implicit val encodeIdent: Encoder[Ident] = Encoder.encodeString.contramap(_.id) diff --git a/modules/restapi/src/main/resources/docspell-openapi.yml b/modules/restapi/src/main/resources/docspell-openapi.yml index 563e5687..88eba465 100644 --- a/modules/restapi/src/main/resources/docspell-openapi.yml +++ b/modules/restapi/src/main/resources/docspell-openapi.yml @@ -255,6 +255,8 @@ paths: Return a list of all configured tags. security: - authTokenHeader: [] + parameters: + - $ref: "#/components/parameters/q" responses: 200: description: Ok @@ -329,6 +331,7 @@ paths: - authTokenHeader: [] parameters: - $ref: "#/components/parameters/full" + - $ref: "#/components/parameters/q" responses: 200: description: Ok @@ -421,6 +424,7 @@ paths: - authTokenHeader: [] parameters: - $ref: "#/components/parameters/full" + - $ref: "#/components/parameters/q" responses: 200: description: Ok @@ -511,6 +515,8 @@ paths: Return a list of all configured equipments. security: - authTokenHeader: [] + parameters: + - $ref: "#/components/parameters/q" responses: 200: description: Ok @@ -2169,3 +2175,10 @@ components: required: true schema: type: string + q: + name: q + in: query + description: A query string. + required: false + schema: + type: string diff --git a/modules/restserver/src/main/scala/docspell/restserver/routes/EquipmentRoutes.scala b/modules/restserver/src/main/scala/docspell/restserver/routes/EquipmentRoutes.scala index 6dfeede7..eec9431f 100644 --- a/modules/restserver/src/main/scala/docspell/restserver/routes/EquipmentRoutes.scala +++ b/modules/restserver/src/main/scala/docspell/restserver/routes/EquipmentRoutes.scala @@ -19,9 +19,10 @@ object EquipmentRoutes { import dsl._ HttpRoutes.of { - case GET -> Root => + case req @ GET -> Root => + val q = req.params.get("q").map(_.trim).filter(_.nonEmpty) for { - data <- backend.equipment.findAll(user.account) + data <- backend.equipment.findAll(user.account, q) resp <- Ok(EquipmentList(data.map(mkEquipment).toList)) } yield resp diff --git a/modules/restserver/src/main/scala/docspell/restserver/routes/OrganizationRoutes.scala b/modules/restserver/src/main/scala/docspell/restserver/routes/OrganizationRoutes.scala index dc4fc494..1ce3a882 100644 --- a/modules/restserver/src/main/scala/docspell/restserver/routes/OrganizationRoutes.scala +++ b/modules/restserver/src/main/scala/docspell/restserver/routes/OrganizationRoutes.scala @@ -20,15 +20,16 @@ object OrganizationRoutes { import dsl._ HttpRoutes.of { - case GET -> Root :? FullQueryParamMatcher(full) => + case req @ GET -> Root :? FullQueryParamMatcher(full) => + val q = req.params.get("q").map(_.trim).filter(_.nonEmpty) if (full.getOrElse(false)) { for { - data <- backend.organization.findAllOrg(user.account) + data <- backend.organization.findAllOrg(user.account, q) resp <- Ok(OrganizationList(data.map(mkOrg).toList)) } yield resp } else { for { - data <- backend.organization.findAllOrgRefs(user.account) + data <- backend.organization.findAllOrgRefs(user.account, q) resp <- Ok(ReferenceList(data.map(mkIdName).toList)) } yield resp } diff --git a/modules/restserver/src/main/scala/docspell/restserver/routes/PersonRoutes.scala b/modules/restserver/src/main/scala/docspell/restserver/routes/PersonRoutes.scala index b7fe174a..71d1926f 100644 --- a/modules/restserver/src/main/scala/docspell/restserver/routes/PersonRoutes.scala +++ b/modules/restserver/src/main/scala/docspell/restserver/routes/PersonRoutes.scala @@ -23,15 +23,16 @@ object PersonRoutes { import dsl._ HttpRoutes.of { - case GET -> Root :? FullQueryParamMatcher(full) => + case req @ GET -> Root :? FullQueryParamMatcher(full) => + val q = req.params.get("q").map(_.trim).filter(_.nonEmpty) if (full.getOrElse(false)) { for { - data <- backend.organization.findAllPerson(user.account) + data <- backend.organization.findAllPerson(user.account, q) resp <- Ok(PersonList(data.map(mkPerson).toList)) } yield resp } else { for { - data <- backend.organization.findAllPersonRefs(user.account) + data <- backend.organization.findAllPersonRefs(user.account, q) resp <- Ok(ReferenceList(data.map(mkIdName).toList)) } yield resp } diff --git a/modules/restserver/src/main/scala/docspell/restserver/routes/TagRoutes.scala b/modules/restserver/src/main/scala/docspell/restserver/routes/TagRoutes.scala index 0fc8579e..5a9fdd8e 100644 --- a/modules/restserver/src/main/scala/docspell/restserver/routes/TagRoutes.scala +++ b/modules/restserver/src/main/scala/docspell/restserver/routes/TagRoutes.scala @@ -20,9 +20,10 @@ object TagRoutes { import dsl._ HttpRoutes.of { - case GET -> Root => + case req @ GET -> Root => + val q = req.params.get("q").map(_.trim).filter(_.nonEmpty) for { - all <- backend.tag.findAll(user.account) + all <- backend.tag.findAll(user.account, q) resp <- Ok(TagList(all.size, all.map(mkTag).toList)) } yield resp diff --git a/modules/store/src/main/scala/docspell/store/queries/QOrganization.scala b/modules/store/src/main/scala/docspell/store/queries/QOrganization.scala index bb89bc30..e9e00631 100644 --- a/modules/store/src/main/scala/docspell/store/queries/QOrganization.scala +++ b/modules/store/src/main/scala/docspell/store/queries/QOrganization.scala @@ -3,9 +3,11 @@ package docspell.store.queries import fs2._ import cats.implicits._ import doobie._ +import doobie.implicits._ import docspell.common._ import docspell.store.{AddResult, Store} import docspell.store.impl.Column +import docspell.store.impl.Implicits._ import docspell.store.records.ROrganization.{Columns => OC} import docspell.store.records.RPerson.{Columns => PC} import docspell.store.records._ @@ -14,16 +16,75 @@ object QOrganization { def findOrgAndContact( coll: Ident, + query: Option[String], order: OC.type => Column - ): Stream[ConnectionIO, (ROrganization, Vector[RContact])] = - ROrganization - .findAll(coll, order) - .evalMap(ro => RContact.findAllOrg(ro.oid).map(cs => (ro, cs))) + ): Stream[ConnectionIO, (ROrganization, Vector[RContact])] = { + val oColl = ROrganization.Columns.cid.prefix("o") + val oName = ROrganization.Columns.name.prefix("o") + val oNotes = ROrganization.Columns.notes.prefix("o") + val oId = ROrganization.Columns.oid.prefix("o") + val cOrg = RContact.Columns.orgId.prefix("c") + val cVal = RContact.Columns.value.prefix("c") + + val cols = ROrganization.Columns.all.map(_.prefix("o")) ++ RContact.Columns.all + .map(_.prefix("c")) + val from = ROrganization.table ++ fr"o LEFT JOIN" ++ + RContact.table ++ fr"c ON" ++ cOrg.is(oId) + + val q = Seq(oColl.is(coll)) ++ (query match { + case Some(str) => + val v = s"%$str%" + Seq(or(cVal.lowerLike(v), oName.lowerLike(v), oNotes.lowerLike(v))) + case None => + Seq.empty + }) + + (selectSimple(cols, from, and(q)) ++ orderBy(order(OC).f)) + .query[(ROrganization, Option[RContact])] + .stream + .groupAdjacentBy(_._1) + .map({ + case (ro, chunk) => + val cs = chunk.toVector.flatMap(_._2) + (ro, cs) + }) + } + def findPersonAndContact( coll: Ident, + query: Option[String], order: PC.type => Column - ): Stream[ConnectionIO, (RPerson, Vector[RContact])] = - RPerson.findAll(coll, order).evalMap(ro => RContact.findAllPerson(ro.pid).map(cs => (ro, cs))) + ): Stream[ConnectionIO, (RPerson, Vector[RContact])] = { + val pColl = PC.cid.prefix("p") + val pName = RPerson.Columns.name.prefix("p") + val pNotes = RPerson.Columns.notes.prefix("p") + val pId = RPerson.Columns.pid.prefix("p") + val cPers = RContact.Columns.personId.prefix("c") + val cVal = RContact.Columns.value.prefix("c") + + val cols = RPerson.Columns.all.map(_.prefix("p")) ++ RContact.Columns.all + .map(_.prefix("c")) + val from = RPerson.table ++ fr"p LEFT JOIN" ++ + RContact.table ++ fr"c ON" ++ cPers.is(pId) + + val q = Seq(pColl.is(coll)) ++ (query match { + case Some(str) => + val v = s"%${str.toLowerCase}%" + Seq(or(cVal.lowerLike(v), pName.lowerLike(v), pNotes.lowerLike(v))) + case None => + Seq.empty + }) + + (selectSimple(cols, from, and(q)) ++ orderBy(order(PC).f)) + .query[(RPerson, Option[RContact])] + .stream + .groupAdjacentBy(_._1) + .map({ + case (ro, chunk) => + val cs = chunk.toVector.flatMap(_._2) + (ro, cs) + }) + } def addOrg[F[_]]( org: ROrganization, diff --git a/modules/store/src/main/scala/docspell/store/records/REquipment.scala b/modules/store/src/main/scala/docspell/store/records/REquipment.scala index 964bec4f..ed2b8d0f 100644 --- a/modules/store/src/main/scala/docspell/store/records/REquipment.scala +++ b/modules/store/src/main/scala/docspell/store/records/REquipment.scala @@ -47,8 +47,16 @@ object REquipment { sql.query[REquipment].option } - def findAll(coll: Ident, order: Columns.type => Column): ConnectionIO[Vector[REquipment]] = { - val sql = selectSimple(all, table, cid.is(coll)) ++ orderBy(order(Columns).f) + def findAll( + coll: Ident, + nameQ: Option[String], + order: Columns.type => Column + ): ConnectionIO[Vector[REquipment]] = { + val q = Seq(cid.is(coll)) ++ (nameQ match { + case Some(str) => Seq(name.lowerLike(s"%${str.toLowerCase}%")) + case None => Seq.empty + }) + val sql = selectSimple(all, table, and(q)) ++ orderBy(order(Columns).f) sql.query[REquipment].to[Vector] } diff --git a/modules/store/src/main/scala/docspell/store/records/ROrganization.scala b/modules/store/src/main/scala/docspell/store/records/ROrganization.scala index d4b333ee..13a84f29 100644 --- a/modules/store/src/main/scala/docspell/store/records/ROrganization.scala +++ b/modules/store/src/main/scala/docspell/store/records/ROrganization.scala @@ -1,5 +1,6 @@ package docspell.store.records +import cats.Eq import fs2.Stream import doobie._ import doobie.implicits._ @@ -20,6 +21,8 @@ case class ROrganization( ) {} object ROrganization { + implicit val orgEq: Eq[ROrganization] = + Eq.by[ROrganization, Ident](_.oid) val table = fr"organization" @@ -105,8 +108,16 @@ object ROrganization { sql.query[ROrganization].stream } - def findAllRef(coll: Ident, order: Columns.type => Column): ConnectionIO[Vector[IdRef]] = { - val sql = selectSimple(List(oid, name), table, cid.is(coll)) ++ orderBy(order(Columns).f) + def findAllRef( + coll: Ident, + nameQ: Option[String], + order: Columns.type => Column + ): ConnectionIO[Vector[IdRef]] = { + val q = Seq(cid.is(coll)) ++ (nameQ match { + case Some(str) => Seq(name.lowerLike(s"%${str.toLowerCase}%")) + case None => Seq.empty + }) + val sql = selectSimple(List(oid, name), table, and(q)) ++ orderBy(order(Columns).f) sql.query[IdRef].to[Vector] } diff --git a/modules/store/src/main/scala/docspell/store/records/RPerson.scala b/modules/store/src/main/scala/docspell/store/records/RPerson.scala index 184367b2..0497b0c0 100644 --- a/modules/store/src/main/scala/docspell/store/records/RPerson.scala +++ b/modules/store/src/main/scala/docspell/store/records/RPerson.scala @@ -1,6 +1,7 @@ package docspell.store.records import fs2.Stream +import cats.Eq import doobie._ import doobie.implicits._ import docspell.common.{IdRef, _} @@ -21,6 +22,8 @@ case class RPerson( ) {} object RPerson { + implicit val personEq: Eq[RPerson] = + Eq.by(_.pid) val table = fr"person" @@ -116,8 +119,16 @@ object RPerson { sql.query[RPerson].stream } - def findAllRef(coll: Ident, order: Columns.type => Column): ConnectionIO[Vector[IdRef]] = { - val sql = selectSimple(List(pid, name), table, cid.is(coll)) ++ orderBy(order(Columns).f) + def findAllRef( + coll: Ident, + nameQ: Option[String], + order: Columns.type => Column + ): ConnectionIO[Vector[IdRef]] = { + val q = Seq(cid.is(coll)) ++ (nameQ match { + case Some(str) => Seq(name.lowerLike(s"%${str.toLowerCase}%")) + case None => Seq.empty + }) + val sql = selectSimple(List(pid, name), table, and(q)) ++ orderBy(order(Columns).f) sql.query[IdRef].to[Vector] } diff --git a/modules/store/src/main/scala/docspell/store/records/RTag.scala b/modules/store/src/main/scala/docspell/store/records/RTag.scala index 52ac5f2c..a702515c 100644 --- a/modules/store/src/main/scala/docspell/store/records/RTag.scala +++ b/modules/store/src/main/scala/docspell/store/records/RTag.scala @@ -65,8 +65,16 @@ object RTag { sql.query[Int].unique.map(_ > 0) } - def findAll(coll: Ident, order: Columns.type => Column): ConnectionIO[Vector[RTag]] = { - val sql = selectSimple(all, table, cid.is(coll)) ++ orderBy(order(Columns).f) + def findAll( + coll: Ident, + nameQ: Option[String], + order: Columns.type => Column + ): ConnectionIO[Vector[RTag]] = { + val q = Seq(cid.is(coll)) ++ (nameQ match { + case Some(str) => Seq(name.lowerLike(s"%${str.toLowerCase}%")) + case None => Seq.empty + }) + val sql = selectSimple(all, table, and(q)) ++ orderBy(order(Columns).f) sql.query[RTag].to[Vector] }