From 8d35d100d6265706e956cf1cece90eb3efe4564e Mon Sep 17 00:00:00 2001 From: Eike Kettner Date: Mon, 16 Nov 2020 23:27:26 +0100 Subject: [PATCH] Change custom fields for multiple items --- .../docspell/backend/ops/OCustomFields.scala | 31 ++++++-- .../src/main/resources/docspell-openapi.yml | 77 ++++++++++++++++++- .../restserver/routes/CustomFieldRoutes.scala | 2 +- .../restserver/routes/ItemMultiRoutes.scala | 24 ++++++ .../restserver/routes/ItemRoutes.scala | 2 +- .../store/records/RCustomFieldValue.scala | 6 +- .../scala/docspell/store/records/RItem.scala | 9 +++ 7 files changed, 137 insertions(+), 14 deletions(-) diff --git a/modules/backend/src/main/scala/docspell/backend/ops/OCustomFields.scala b/modules/backend/src/main/scala/docspell/backend/ops/OCustomFields.scala index 4792a4d9..8f6179d0 100644 --- a/modules/backend/src/main/scala/docspell/backend/ops/OCustomFields.scala +++ b/modules/backend/src/main/scala/docspell/backend/ops/OCustomFields.scala @@ -1,6 +1,7 @@ package docspell.backend.ops import cats.data.EitherT +import cats.data.NonEmptyList import cats.data.OptionT import cats.effect._ import cats.implicits._ @@ -20,6 +21,7 @@ import docspell.store.records.RCustomFieldValue import docspell.store.records.RItem import doobie._ +import org.log4s.getLogger trait OCustomFields[F[_]] { @@ -41,6 +43,8 @@ trait OCustomFields[F[_]] { /** Sets a value given a field an an item. Existing values are overwritten. */ def setValue(item: Ident, value: SetValue): F[SetValueResult] + def setValueMultiple(items: NonEmptyList[Ident], value: SetValue): F[SetValueResult] + /** Deletes a value for a given field an item. */ def deleteValue(in: RemoveValue): F[UpdateResult] } @@ -79,7 +83,7 @@ object OCustomFields { case class RemoveValue( field: Ident, - item: Ident, + item: NonEmptyList[Ident], collective: Ident ) @@ -88,8 +92,10 @@ object OCustomFields { ): Resource[F, OCustomFields[F]] = Resource.pure[F, OCustomFields[F]](new OCustomFields[F] { + private[this] val logger = Logger.log4s[ConnectionIO](getLogger) + def findAll(coll: Ident, nameQuery: Option[String]): F[Vector[CustomFieldData]] = - store.transact(QCustomField.findAllLike(coll, nameQuery)) + store.transact(QCustomField.findAllLike(coll, nameQuery.filter(_.nonEmpty))) def findById(coll: Ident, field: Ident): F[Option[CustomFieldData]] = store.transact(QCustomField.findById(field, coll)) @@ -113,6 +119,7 @@ object OCustomFields { val update = for { field <- OptionT(RCustomField.findByIdOrName(fieldIdOrName, coll)) + _ <- OptionT.liftF(logger.info(s"Deleting field: $field")) n <- OptionT.liftF(RCustomFieldValue.deleteByField(field.id)) k <- OptionT.liftF(RCustomField.deleteById(field.id, coll)) } yield n + k @@ -121,24 +128,32 @@ object OCustomFields { } def setValue(item: Ident, value: SetValue): F[SetValueResult] = + setValueMultiple(NonEmptyList.of(item), value) + + def setValueMultiple( + items: NonEmptyList[Ident], + value: SetValue + ): F[SetValueResult] = (for { field <- EitherT.fromOptionF( store.transact(RCustomField.findByIdOrName(value.field, value.collective)), SetValueResult.fieldNotFound ) - _ <- EitherT( - store - .transact(RItem.existsByIdAndCollective(item, value.collective)) - .map(flag => if (flag) Right(()) else Left(SetValueResult.itemNotFound)) - ) fval <- EitherT.fromEither[F]( field.ftype .parseValue(value.value) .leftMap(SetValueResult.valueInvalid) .map(field.ftype.valueString) ) + _ <- EitherT( + store + .transact(RItem.existsByIdsAndCollective(items, value.collective)) + .map(flag => if (flag) Right(()) else Left(SetValueResult.itemNotFound)) + ) nu <- EitherT.right[SetValueResult]( - store.transact(RCustomField.setValue(field, item, fval)) + items + .traverse(item => store.transact(RCustomField.setValue(field, item, fval))) + .map(_.toList.sum) ) } yield nu).fold(identity, _ => SetValueResult.success) diff --git a/modules/restapi/src/main/resources/docspell-openapi.yml b/modules/restapi/src/main/resources/docspell-openapi.yml index c1a849b1..8774a2f7 100644 --- a/modules/restapi/src/main/resources/docspell-openapi.yml +++ b/modules/restapi/src/main/resources/docspell-openapi.yml @@ -2383,6 +2383,52 @@ paths: schema: $ref: "#/components/schemas/BasicResult" + /sec/items/customfield: + put: + tags: [ Item (Multi Edit) ] + summary: Set the value of a custom field for multiple items + description: | + Sets the value for a custom field to multiple given items. If + a value already exists, it is overwritten. + security: + - authTokenHeader: [] + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/ItemsAndFieldValue" + responses: + 200: + description: Ok + content: + application/json: + schema: + $ref: "#/components/schemas/BasicResult" + + /sec/items/customfieldremove: + post: + tags: [ Item (Multi Edit) ] + summary: Removes the value for a custom field on multiple items + description: | + Removes the value for the given custom field from multiple + items. The field may be specified by its id or name. + security: + - authTokenHeader: [] + parameters: + - $ref: "#/components/parameters/itemId" + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/ItemsAndName" + responses: + 200: + description: Ok + content: + application/json: + schema: + $ref: "#/components/schemas/BasicResult" + /sec/attachment/{id}: delete: @@ -3246,7 +3292,7 @@ paths: schema: $ref: "#/components/schemas/BasicResult" - /sec/customfields: + /sec/customfield: get: tags: [ Custom Fields ] summary: Get all defined custom fields. @@ -3283,7 +3329,7 @@ paths: schema: $ref: "#/components/schemas/BasicResult" - /sec/customfields/{id}: + /sec/customfield/{id}: parameters: - $ref: "#/components/parameters/id" get: @@ -3342,6 +3388,21 @@ paths: components: schemas: + ItemsAndFieldValue: + description: | + Holds a list of item ids and a custom field value. + required: + - items + - field + properties: + items: + type: array + items: + type: string + format: ident + field: + $ref: "#/components/schemas/CustomFieldValue" + ItemsAndRefs: description: | Holds a list of item ids and a list of ids of some other @@ -3459,6 +3520,12 @@ components: ftype: type: string format: customfieldtype + enum: + - text + - numeric + - date + - bool + - money CustomField: description: | @@ -3481,6 +3548,12 @@ components: ftype: type: string format: customfieldtype + enum: + - text + - numeric + - date + - bool + - money usages: type: integer format: int32 diff --git a/modules/restserver/src/main/scala/docspell/restserver/routes/CustomFieldRoutes.scala b/modules/restserver/src/main/scala/docspell/restserver/routes/CustomFieldRoutes.scala index ceab2aea..0aab3c17 100644 --- a/modules/restserver/src/main/scala/docspell/restserver/routes/CustomFieldRoutes.scala +++ b/modules/restserver/src/main/scala/docspell/restserver/routes/CustomFieldRoutes.scala @@ -56,7 +56,7 @@ object CustomFieldRoutes { case DELETE -> Root / Ident(id) => for { - res <- backend.customFields.delete(id, user.account.collective) + res <- backend.customFields.delete(user.account.collective, id) resp <- Ok(convertResult(res)) } yield resp } diff --git a/modules/restserver/src/main/scala/docspell/restserver/routes/ItemMultiRoutes.scala b/modules/restserver/src/main/scala/docspell/restserver/routes/ItemMultiRoutes.scala index 7b1dd931..4b15689c 100644 --- a/modules/restserver/src/main/scala/docspell/restserver/routes/ItemMultiRoutes.scala +++ b/modules/restserver/src/main/scala/docspell/restserver/routes/ItemMultiRoutes.scala @@ -8,6 +8,7 @@ import cats.implicits._ import docspell.backend.BackendApp import docspell.backend.auth.AuthToken +import docspell.backend.ops.OCustomFields.{RemoveValue, SetValue} import docspell.common.{Ident, ItemState} import docspell.restapi.model._ import docspell.restserver.conv.Conversions @@ -180,6 +181,29 @@ object ItemMultiRoutes { ) resp <- Ok(res) } yield resp + + case req @ PUT -> Root / "customfield" => + for { + json <- req.as[ItemsAndFieldValue] + items <- readIds[F](json.items) + res <- backend.customFields.setValueMultiple( + items, + SetValue(json.field.field, json.field.value, user.account.collective) + ) + resp <- Ok(Conversions.basicResult(res)) + } yield resp + + case req @ POST -> Root / "customfieldremove" => + for { + json <- req.as[ItemsAndName] + items <- readIds[F](json.items) + field <- readId[F](json.name) + res <- backend.customFields.deleteValue( + RemoveValue(field, items, user.account.collective) + ) + resp <- Ok(Conversions.basicResult(res, "Custom fields removed.")) + } yield resp + } } 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 8f25eb08..c6606667 100644 --- a/modules/restserver/src/main/scala/docspell/restserver/routes/ItemRoutes.scala +++ b/modules/restserver/src/main/scala/docspell/restserver/routes/ItemRoutes.scala @@ -372,7 +372,7 @@ object ItemRoutes { case DELETE -> Root / Ident(id) / "customfield" / Ident(fieldId) => for { res <- backend.customFields.deleteValue( - RemoveValue(fieldId, id, user.account.collective) + RemoveValue(fieldId, NonEmptyList.of(id), user.account.collective) ) resp <- Ok(Conversions.basicResult(res, "Custom field value removed.")) } yield resp diff --git a/modules/store/src/main/scala/docspell/store/records/RCustomFieldValue.scala b/modules/store/src/main/scala/docspell/store/records/RCustomFieldValue.scala index 7789143c..53356233 100644 --- a/modules/store/src/main/scala/docspell/store/records/RCustomFieldValue.scala +++ b/modules/store/src/main/scala/docspell/store/records/RCustomFieldValue.scala @@ -1,5 +1,7 @@ package docspell.store.records +import cats.data.NonEmptyList + import docspell.common._ import docspell.store.impl.Column import docspell.store.impl.Implicits._ @@ -57,6 +59,6 @@ object RCustomFieldValue { def deleteByItem(item: Ident): ConnectionIO[Int] = deleteFrom(table, Columns.itemId.is(item)).update.run - def deleteValue(fieldId: Ident, item: Ident): ConnectionIO[Int] = - deleteFrom(table, and(Columns.id.is(fieldId), Columns.itemId.is(item))).update.run + def deleteValue(fieldId: Ident, items: NonEmptyList[Ident]): ConnectionIO[Int] = + deleteFrom(table, and(Columns.id.is(fieldId), Columns.itemId.isIn(items))).update.run } diff --git a/modules/store/src/main/scala/docspell/store/records/RItem.scala b/modules/store/src/main/scala/docspell/store/records/RItem.scala index 89f78e2a..980cd324 100644 --- a/modules/store/src/main/scala/docspell/store/records/RItem.scala +++ b/modules/store/src/main/scala/docspell/store/records/RItem.scala @@ -326,6 +326,15 @@ object RItem { def existsByIdAndCollective(itemId: Ident, coll: Ident): ConnectionIO[Boolean] = selectCount(id, table, and(id.is(itemId), cid.is(coll))).query[Int].unique.map(_ > 0) + def existsByIdsAndCollective( + itemIds: NonEmptyList[Ident], + coll: Ident + ): ConnectionIO[Boolean] = + selectCount(id, table, and(id.isIn(itemIds), cid.is(coll))) + .query[Int] + .unique + .map(_ == itemIds.size) + def findByIdAndCollective(itemId: Ident, coll: Ident): ConnectionIO[Option[RItem]] = selectSimple(all, table, and(id.is(itemId), cid.is(coll))).query[RItem].option