From 066c856981a697f33b97d31f6939f3218eb7e67e Mon Sep 17 00:00:00 2001 From: Eike Kettner <eike.kettner@posteo.de> Date: Sun, 22 Nov 2020 18:25:27 +0100 Subject: [PATCH] Allow to search for custom field values --- .../docspell/backend/ops/OItemSearch.scala | 3 ++ .../src/main/resources/docspell-openapi.yml | 5 ++ .../restserver/conv/Conversions.scala | 4 ++ .../scala/docspell/store/queries/QItem.scala | 50 +++++++++++++++++-- 4 files changed, 58 insertions(+), 4 deletions(-) diff --git a/modules/backend/src/main/scala/docspell/backend/ops/OItemSearch.scala b/modules/backend/src/main/scala/docspell/backend/ops/OItemSearch.scala index 41870dce..c546a184 100644 --- a/modules/backend/src/main/scala/docspell/backend/ops/OItemSearch.scala +++ b/modules/backend/src/main/scala/docspell/backend/ops/OItemSearch.scala @@ -53,6 +53,9 @@ trait OItemSearch[F[_]] { object OItemSearch { + type CustomValue = QItem.CustomValue + val CustomValue = QItem.CustomValue + type Query = QItem.Query val Query = QItem.Query diff --git a/modules/restapi/src/main/resources/docspell-openapi.yml b/modules/restapi/src/main/resources/docspell-openapi.yml index 8426f129..ed0df51b 100644 --- a/modules/restapi/src/main/resources/docspell-openapi.yml +++ b/modules/restapi/src/main/resources/docspell-openapi.yml @@ -4952,6 +4952,7 @@ components: - inbox - offset - limit + - customValues properties: tagsInclude: type: array @@ -5031,6 +5032,10 @@ components: format: date-time itemSubset: $ref: "#/components/schemas/IdList" + customValues: + type: array + items: + $ref: "#/components/schemas/CustomFieldValue" ItemLight: description: | An item with only a few important properties. diff --git a/modules/restserver/src/main/scala/docspell/restserver/conv/Conversions.scala b/modules/restserver/src/main/scala/docspell/restserver/conv/Conversions.scala index 2f7f0378..e5d49fc2 100644 --- a/modules/restserver/src/main/scala/docspell/restserver/conv/Conversions.scala +++ b/modules/restserver/src/main/scala/docspell/restserver/conv/Conversions.scala @@ -143,9 +143,13 @@ trait Conversions { m.itemSubset .map(_.ids.flatMap(i => Ident.fromString(i).toOption).toSet) .filter(_.nonEmpty), + m.customValues.map(mkCustomValue), None ) + def mkCustomValue(v: CustomFieldValue): OItemSearch.CustomValue = + OItemSearch.CustomValue(v.field, v.value) + def mkItemList(v: Vector[OItemSearch.ListItem]): ItemLightList = { val groups = v.groupBy(item => item.date.toUtcDate.toString.substring(0, 7)) 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 91deca4d..f587ae9f 100644 --- a/modules/store/src/main/scala/docspell/store/queries/QItem.scala +++ b/modules/store/src/main/scala/docspell/store/queries/QItem.scala @@ -139,7 +139,7 @@ object QItem { val sources = RAttachmentSource.findByItemWithMeta(id) val archives = RAttachmentArchive.findByItemWithMeta(id) val tags = RTag.findByItem(id) - val customfields = findCustomFieldValues(id) + val customfields = findCustomFieldValuesForItem(id) for { data <- q @@ -153,7 +153,9 @@ object QItem { ) } - def findCustomFieldValues(itemId: Ident): ConnectionIO[Vector[ItemFieldValue]] = { + def findCustomFieldValuesForItem( + itemId: Ident + ): ConnectionIO[Vector[ItemFieldValue]] = { val cfId = RCustomField.Columns.id.prefix("cf") val cfName = RCustomField.Columns.name.prefix("cf") val cfLabel = RCustomField.Columns.label.prefix("cf") @@ -191,6 +193,8 @@ object QItem { notes: Option[String] ) + case class CustomValue(field: Ident, value: String) + case class Query( account: AccountId, name: Option[String], @@ -211,6 +215,7 @@ object QItem { dueDateTo: Option[Timestamp], allNames: Option[String], itemIds: Option[Set[Ident]], + customValues: Seq[CustomValue], orderAsc: Option[RItem.Columns.type => Column] ) @@ -236,6 +241,7 @@ object QItem { None, None, None, + Seq.empty, None ) } @@ -261,6 +267,35 @@ object QItem { Batch(0, c) } + private def findCustomFieldValuesForColl( + coll: Ident, + cv: Seq[CustomValue] + ): Seq[(String, Fragment)] = { + val cfId = RCustomField.Columns.id.prefix("cf") + val cfName = RCustomField.Columns.name.prefix("cf") + val cfColl = RCustomField.Columns.cid.prefix("cf") + val cvValue = RCustomFieldValue.Columns.value.prefix("cvf") + val cvField = RCustomFieldValue.Columns.field.prefix("cvf") + val cvItem = RCustomFieldValue.Columns.itemId.prefix("cvf") + + val cfFrom = + RCustomFieldValue.table ++ fr"cvf INNER JOIN" ++ RCustomField.table ++ fr"cf ON" ++ cvField + .is(cfId) + + def singleSelect(v: CustomValue) = + selectSimple( + Seq(cvItem), + cfFrom, + and( + cfColl.is(coll), + or(cfName.is(v.field), cfId.is(v.field)), + cvValue.is(v.value) + ) + ) + if (cv.isEmpty) Seq.empty + else Seq("customvalues" -> cv.map(singleSelect).reduce(_ ++ fr"INTERSECT" ++ _)) + } + private def findItemsBase( q: Query, distinct: Boolean, @@ -279,6 +314,7 @@ object QItem { val orgCols = List(OC.oid, OC.name) val equipCols = List(EC.eid, EC.name) val folderCols = List(FC.id, FC.name) + val cvItem = RCustomFieldValue.Columns.itemId.prefix("cv") val finalCols = commas( Seq( @@ -325,6 +361,9 @@ object QItem { val withAttach = fr"SELECT COUNT(" ++ AC.id.f ++ fr") as num, " ++ AC.itemId.f ++ fr"from" ++ RAttachment.table ++ fr"GROUP BY (" ++ AC.itemId.f ++ fr")" + val withCustomValues = + findCustomFieldValuesForColl(q.account.collective, q.customValues) + val selectKW = if (distinct) fr"SELECT DISTINCT" else fr"SELECT" withCTE( (Seq( @@ -334,7 +373,7 @@ object QItem { "equips" -> withEquips, "attachs" -> withAttach, "folders" -> withFolder - ) ++ ctes): _* + ) ++ withCustomValues ++ ctes): _* ) ++ selectKW ++ finalCols ++ fr" FROM items i" ++ fr"LEFT JOIN attachs a ON" ++ IC.id.prefix("i").is(AC.itemId.prefix("a")) ++ @@ -344,7 +383,10 @@ object QItem { fr"LEFT JOIN equips e1 ON" ++ IC.concEquipment .prefix("i") .is(EC.eid.prefix("e1")) ++ - fr"LEFT JOIN folders f1 ON" ++ IC.folder.prefix("i").is(FC.id.prefix("f1")) + fr"LEFT JOIN folders f1 ON" ++ IC.folder.prefix("i").is(FC.id.prefix("f1")) ++ + (if (q.customValues.isEmpty) Fragment.empty + else + fr"INNER JOIN customvalues cv ON" ++ cvItem.is(IC.id.prefix("i"))) } def findItems(