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 a108f0bd..6b1ecd98 100644 --- a/modules/backend/src/main/scala/docspell/backend/ops/OItem.scala +++ b/modules/backend/src/main/scala/docspell/backend/ops/OItem.scala @@ -43,8 +43,12 @@ trait OItem[F[_]] { collective: Ident ): F[Option[AttachmentArchiveData[F]]] + /** Sets the given tags (removing all existing ones). */ def setTags(item: Ident, tagIds: List[Ident], collective: Ident): F[AddResult] + /** Create a new tag and add it to the item. */ + def addNewTag(item: Ident, tag: RTag): F[AddResult] + def setDirection(item: Ident, direction: Direction, collective: Ident): F[AddResult] def setCorrOrg(item: Ident, org: Option[Ident], collective: Ident): F[AddResult] @@ -132,220 +136,244 @@ object OItem { } def apply[F[_]: Effect](store: Store[F]): Resource[F, OItem[F]] = - Resource.pure[F, OItem[F]](new OItem[F] { + for { + otag <- OTag(store) + oitem <- Resource.pure[F, OItem[F]](new OItem[F] { + def moveAttachmentBefore( + itemId: Ident, + source: Ident, + target: Ident + ): F[AddResult] = + store + .transact(QItem.moveAttachmentBefore(itemId, source, target)) + .attempt + .map(AddResult.fromUpdate) - def moveAttachmentBefore( - itemId: Ident, - source: Ident, - target: Ident - ): F[AddResult] = - store - .transact(QItem.moveAttachmentBefore(itemId, source, target)) - .attempt - .map(AddResult.fromUpdate) + def findItem(id: Ident, collective: Ident): F[Option[ItemData]] = + store + .transact(QItem.findItem(id)) + .map(opt => opt.flatMap(_.filterCollective(collective))) - def findItem(id: Ident, collective: Ident): F[Option[ItemData]] = - store - .transact(QItem.findItem(id)) - .map(opt => opt.flatMap(_.filterCollective(collective))) + def findItems(q: Query, batch: Batch): F[Vector[ListItem]] = + store + .transact(QItem.findItems(q, batch).take(batch.limit.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 findItemsWithTags(q: Query, batch: Batch): F[Vector[ListItemWithTags]] = + store + .transact(QItem.findItemsWithTags(q, batch).take(batch.limit.toLong)) + .compile + .toVector - def findItemsWithTags(q: Query, batch: Batch): F[Vector[ListItemWithTags]] = - store - .transact(QItem.findItemsWithTags(q, batch).take(batch.limit.toLong)) - .compile - .toVector + def findAttachment(id: Ident, collective: Ident): F[Option[AttachmentData[F]]] = + store + .transact(RAttachment.findByIdAndCollective(id, collective)) + .flatMap({ + case Some(ra) => + makeBinaryData(ra.fileId) { m => + AttachmentData[F]( + ra, + m, + store.bitpeace.fetchData2(RangeDef.all)(Stream.emit(m)) + ) + } - def findAttachment(id: Ident, collective: Ident): F[Option[AttachmentData[F]]] = - store - .transact(RAttachment.findByIdAndCollective(id, collective)) - .flatMap({ - case Some(ra) => - makeBinaryData(ra.fileId) { m => - AttachmentData[F]( - ra, - m, - store.bitpeace.fetchData2(RangeDef.all)(Stream.emit(m)) + case None => + (None: Option[AttachmentData[F]]).pure[F] + }) + + def findAttachmentSource( + id: Ident, + collective: Ident + ): F[Option[AttachmentSourceData[F]]] = + store + .transact(RAttachmentSource.findByIdAndCollective(id, collective)) + .flatMap({ + case Some(ra) => + makeBinaryData(ra.fileId) { m => + AttachmentSourceData[F]( + ra, + m, + store.bitpeace.fetchData2(RangeDef.all)(Stream.emit(m)) + ) + } + + case None => + (None: Option[AttachmentSourceData[F]]).pure[F] + }) + + def findAttachmentArchive( + id: Ident, + collective: Ident + ): F[Option[AttachmentArchiveData[F]]] = + store + .transact(RAttachmentArchive.findByIdAndCollective(id, collective)) + .flatMap({ + case Some(ra) => + makeBinaryData(ra.fileId) { m => + AttachmentArchiveData[F]( + ra, + m, + store.bitpeace.fetchData2(RangeDef.all)(Stream.emit(m)) + ) + } + + case None => + (None: Option[AttachmentArchiveData[F]]).pure[F] + }) + + private def makeBinaryData[A](fileId: Ident)(f: FileMeta => A): F[Option[A]] = + store.bitpeace + .get(fileId.id) + .unNoneTerminate + .compile + .last + .map( + _.map(m => f(m)) + ) + + def setTags(item: Ident, tagIds: List[Ident], collective: Ident): F[AddResult] = { + val db = for { + cid <- RItem.getCollective(item) + nd <- + if (cid.contains(collective)) RTagItem.deleteItemTags(item) + else 0.pure[ConnectionIO] + ni <- + if (tagIds.nonEmpty && cid.contains(collective)) + RTagItem.insertItemTags(item, tagIds) + else 0.pure[ConnectionIO] + } yield nd + ni + + store.transact(db).attempt.map(AddResult.fromUpdate) + } + + def addNewTag(item: Ident, tag: RTag): F[AddResult] = + (for { + _ <- OptionT(store.transact(RItem.getCollective(item))) + .filter(_ == tag.collective) + addres <- OptionT.liftF(otag.add(tag)) + _ <- addres match { + case AddResult.Success => + OptionT.liftF( + store.transact(RTagItem.insertItemTags(item, List(tag.tagId))) ) - } + case AddResult.EntityExists(_) => + OptionT.pure[F](0) + case AddResult.Failure(_) => + OptionT.pure[F](0) + } + } yield addres) + .getOrElse(AddResult.Failure(new Exception("Collective mismatch"))) - case None => - (None: Option[AttachmentData[F]]).pure[F] - }) + def setDirection( + item: Ident, + direction: Direction, + collective: Ident + ): F[AddResult] = + store + .transact(RItem.updateDirection(item, collective, direction)) + .attempt + .map(AddResult.fromUpdate) - def findAttachmentSource( - id: Ident, - collective: Ident - ): F[Option[AttachmentSourceData[F]]] = - store - .transact(RAttachmentSource.findByIdAndCollective(id, collective)) - .flatMap({ - case Some(ra) => - makeBinaryData(ra.fileId) { m => - AttachmentSourceData[F]( - ra, - m, - store.bitpeace.fetchData2(RangeDef.all)(Stream.emit(m)) - ) - } + def setCorrOrg(item: Ident, org: Option[Ident], collective: Ident): F[AddResult] = + store + .transact(RItem.updateCorrOrg(item, collective, org)) + .attempt + .map(AddResult.fromUpdate) - case None => - (None: Option[AttachmentSourceData[F]]).pure[F] - }) + def setCorrPerson( + item: Ident, + person: Option[Ident], + collective: Ident + ): F[AddResult] = + store + .transact(RItem.updateCorrPerson(item, collective, person)) + .attempt + .map(AddResult.fromUpdate) - def findAttachmentArchive( - id: Ident, - collective: Ident - ): F[Option[AttachmentArchiveData[F]]] = - store - .transact(RAttachmentArchive.findByIdAndCollective(id, collective)) - .flatMap({ - case Some(ra) => - makeBinaryData(ra.fileId) { m => - AttachmentArchiveData[F]( - ra, - m, - store.bitpeace.fetchData2(RangeDef.all)(Stream.emit(m)) - ) - } + def setConcPerson( + item: Ident, + person: Option[Ident], + collective: Ident + ): F[AddResult] = + store + .transact(RItem.updateConcPerson(item, collective, person)) + .attempt + .map(AddResult.fromUpdate) - case None => - (None: Option[AttachmentArchiveData[F]]).pure[F] - }) + def setConcEquip( + item: Ident, + equip: Option[Ident], + collective: Ident + ): F[AddResult] = + store + .transact(RItem.updateConcEquip(item, collective, equip)) + .attempt + .map(AddResult.fromUpdate) - private def makeBinaryData[A](fileId: Ident)(f: FileMeta => A): F[Option[A]] = - store.bitpeace - .get(fileId.id) - .unNoneTerminate - .compile - .last - .map( - _.map(m => f(m)) - ) + def setNotes( + item: Ident, + notes: Option[String], + collective: Ident + ): F[AddResult] = + store + .transact(RItem.updateNotes(item, collective, notes)) + .attempt + .map(AddResult.fromUpdate) - def setTags(item: Ident, tagIds: List[Ident], collective: Ident): F[AddResult] = { - val db = for { - cid <- RItem.getCollective(item) - nd <- - if (cid.contains(collective)) RTagItem.deleteItemTags(item) - else 0.pure[ConnectionIO] - ni <- - if (tagIds.nonEmpty && cid.contains(collective)) - RTagItem.insertItemTags(item, tagIds) - else 0.pure[ConnectionIO] - } yield nd + ni + def setName(item: Ident, name: String, collective: Ident): F[AddResult] = + store + .transact(RItem.updateName(item, collective, name)) + .attempt + .map(AddResult.fromUpdate) - store.transact(db).attempt.map(AddResult.fromUpdate) - } + def setState(item: Ident, state: ItemState, collective: Ident): F[AddResult] = + store + .transact(RItem.updateStateForCollective(item, state, collective)) + .attempt + .map(AddResult.fromUpdate) - def setDirection( - item: Ident, - direction: Direction, - collective: Ident - ): F[AddResult] = - store - .transact(RItem.updateDirection(item, collective, direction)) - .attempt - .map(AddResult.fromUpdate) + def setItemDate( + item: Ident, + date: Option[Timestamp], + collective: Ident + ): F[AddResult] = + store + .transact(RItem.updateDate(item, collective, date)) + .attempt + .map(AddResult.fromUpdate) - def setCorrOrg(item: Ident, org: Option[Ident], collective: Ident): F[AddResult] = - store - .transact(RItem.updateCorrOrg(item, collective, org)) - .attempt - .map(AddResult.fromUpdate) + def setItemDueDate( + item: Ident, + date: Option[Timestamp], + collective: Ident + ): F[AddResult] = + store + .transact(RItem.updateDueDate(item, collective, date)) + .attempt + .map(AddResult.fromUpdate) - def setCorrPerson( - item: Ident, - person: Option[Ident], - collective: Ident - ): F[AddResult] = - store - .transact(RItem.updateCorrPerson(item, collective, person)) - .attempt - .map(AddResult.fromUpdate) + def deleteItem(itemId: Ident, collective: Ident): F[Int] = + QItem.delete(store)(itemId, collective) - def setConcPerson( - item: Ident, - person: Option[Ident], - collective: Ident - ): F[AddResult] = - store - .transact(RItem.updateConcPerson(item, collective, person)) - .attempt - .map(AddResult.fromUpdate) + def getProposals(item: Ident, collective: Ident): F[MetaProposalList] = + store.transact(QAttachment.getMetaProposals(item, collective)) - def setConcEquip( - item: Ident, - equip: Option[Ident], - collective: Ident - ): F[AddResult] = - store - .transact(RItem.updateConcEquip(item, collective, equip)) - .attempt - .map(AddResult.fromUpdate) + def findAttachmentMeta(id: Ident, collective: Ident): F[Option[RAttachmentMeta]] = + store.transact(QAttachment.getAttachmentMeta(id, collective)) - def setNotes(item: Ident, notes: Option[String], collective: Ident): F[AddResult] = - store - .transact(RItem.updateNotes(item, collective, notes)) - .attempt - .map(AddResult.fromUpdate) + def findByFileCollective(checksum: String, collective: Ident): F[Vector[RItem]] = + store.transact(QItem.findByChecksum(checksum, collective)) - def setName(item: Ident, name: String, collective: Ident): F[AddResult] = - store - .transact(RItem.updateName(item, collective, name)) - .attempt - .map(AddResult.fromUpdate) + def findByFileSource(checksum: String, sourceId: Ident): F[Vector[RItem]] = + store.transact((for { + coll <- OptionT(RSource.findCollective(sourceId)) + items <- OptionT.liftF(QItem.findByChecksum(checksum, coll)) + } yield items).getOrElse(Vector.empty)) - def setState(item: Ident, state: ItemState, collective: Ident): F[AddResult] = - store - .transact(RItem.updateStateForCollective(item, state, collective)) - .attempt - .map(AddResult.fromUpdate) - - def setItemDate( - item: Ident, - date: Option[Timestamp], - collective: Ident - ): F[AddResult] = - store - .transact(RItem.updateDate(item, collective, date)) - .attempt - .map(AddResult.fromUpdate) - - def setItemDueDate( - item: Ident, - date: Option[Timestamp], - collective: Ident - ): F[AddResult] = - store - .transact(RItem.updateDueDate(item, collective, date)) - .attempt - .map(AddResult.fromUpdate) - - def deleteItem(itemId: Ident, collective: Ident): F[Int] = - QItem.delete(store)(itemId, collective) - - def getProposals(item: Ident, collective: Ident): F[MetaProposalList] = - store.transact(QAttachment.getMetaProposals(item, collective)) - - def findAttachmentMeta(id: Ident, collective: Ident): F[Option[RAttachmentMeta]] = - store.transact(QAttachment.getAttachmentMeta(id, collective)) - - def findByFileCollective(checksum: String, collective: Ident): F[Vector[RItem]] = - store.transact(QItem.findByChecksum(checksum, collective)) - - def findByFileSource(checksum: String, sourceId: Ident): F[Vector[RItem]] = - store.transact((for { - coll <- OptionT(RSource.findCollective(sourceId)) - items <- OptionT.liftF(QItem.findByChecksum(checksum, coll)) - } yield items).getOrElse(Vector.empty)) - - def deleteAttachment(id: Ident, collective: Ident): F[Int] = - QAttachment.deleteSingleAttachment(store)(id, collective) - }) + def deleteAttachment(id: Ident, collective: Ident): F[Int] = + QAttachment.deleteSingleAttachment(store)(id, collective) + }) + } yield oitem } diff --git a/modules/restapi/src/main/resources/docspell-openapi.yml b/modules/restapi/src/main/resources/docspell-openapi.yml index 25abffca..cb32158e 100644 --- a/modules/restapi/src/main/resources/docspell-openapi.yml +++ b/modules/restapi/src/main/resources/docspell-openapi.yml @@ -1081,6 +1081,31 @@ paths: application/json: schema: $ref: "#/components/schemas/BasicResult" + post: + tags: [ Item ] + summary: Add a new tag to an item. + description: | + Creates a new tag and associates it to the given item. + + The tag's `id` and `created` are generated and not used from + the given data, so it can be left empty. Only `name` and + `category` are used, where `category` is optional. + security: + - authTokenHeader: [] + parameters: + - $ref: "#/components/parameters/id" + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/Tag" + responses: + 200: + description: Ok + content: + application/json: + schema: + $ref: "#/components/schemas/BasicResult" /sec/item/{id}/direction: put: tags: [ Item ] 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 f2511f2e..60a7e3a7 100644 --- a/modules/restserver/src/main/scala/docspell/restserver/routes/ItemRoutes.scala +++ b/modules/restserver/src/main/scala/docspell/restserver/routes/ItemRoutes.scala @@ -78,6 +78,14 @@ object ItemRoutes { resp <- Ok(Conversions.basicResult(res, "Tags updated")) } yield resp + case req @ POST -> Root / Ident(id) / "tags" => + for { + data <- req.as[Tag] + rtag <- Conversions.newTag(data, user.account.collective) + res <- backend.item.addNewTag(id, rtag) + resp <- Ok(Conversions.basicResult(res, "Tag added.")) + } yield resp + case req @ PUT -> Root / Ident(id) / "direction" => for { dir <- req.as[DirectionValue]