~jan0sch/smederee

Showing details for patch 48fc3bde8b6ce6180ab085cc23a9e6c70f6255eb.
2023-09-12 (Tue), 9:29 AM - Jens Grassel - 48fc3bde8b6ce6180ab085cc23a9e6c70f6255eb

Tickets: Extend milestone repository to be able to load a milestone's tickets.

- add `allTickets` function analogue to the one in the tickets repository
- add test
Summary of changes
3 files modified with 115 lines added and 0 lines removed
  • modules/tickets/src/main/scala/de/smederee/tickets/DoobieMilestoneRepository.scala with 56 added and 0 removed lines
  • modules/tickets/src/main/scala/de/smederee/tickets/MilestoneRepository.scala with 11 added and 0 removed lines
  • modules/tickets/src/test/scala/de/smederee/tickets/DoobieMilestoneRepositoryTest.scala with 48 added and 0 removed lines
diff -rN -u old-smederee/modules/tickets/src/main/scala/de/smederee/tickets/DoobieMilestoneRepository.scala new-smederee/modules/tickets/src/main/scala/de/smederee/tickets/DoobieMilestoneRepository.scala
--- old-smederee/modules/tickets/src/main/scala/de/smederee/tickets/DoobieMilestoneRepository.scala	2025-01-15 09:32:36.548509128 +0000
+++ new-smederee/modules/tickets/src/main/scala/de/smederee/tickets/DoobieMilestoneRepository.scala	2025-01-15 09:32:36.552509135 +0000
@@ -17,7 +17,11 @@
 
 package de.smederee.tickets
 
+import java.util.UUID
+
 import cats.effect._
+import cats.syntax.all._
+import doobie.Fragments._
 import doobie._
 import doobie.implicits._
 import doobie.postgres.implicits._
@@ -29,10 +33,39 @@
 
   given LogHandler[F] = Slf4jLogHandler.createLogHandler[F](log)
 
+  given Meta[AssigneeId]           = Meta[UUID].timap(AssigneeId.apply)(_.toUUID)
+  given Meta[AssigneeName]         = Meta[String].timap(AssigneeName.apply)(_.toString)
+  given Meta[ColourCode]           = Meta[String].timap(ColourCode.apply)(_.toString)
+  given Meta[LabelDescription]     = Meta[String].timap(LabelDescription.apply)(_.toString)
+  given Meta[LabelId]              = Meta[Long].timap(LabelId.apply)(_.toLong)
+  given Meta[LabelName]            = Meta[String].timap(LabelName.apply)(_.toString)
   given Meta[MilestoneDescription] = Meta[String].timap(MilestoneDescription.apply)(_.toString)
   given Meta[MilestoneId]          = Meta[Long].timap(MilestoneId.apply)(_.toLong)
   given Meta[MilestoneTitle]       = Meta[String].timap(MilestoneTitle.apply)(_.toString)
   given Meta[ProjectId]            = Meta[Long].timap(ProjectId.apply)(_.toLong)
+  given Meta[SubmitterId]          = Meta[UUID].timap(SubmitterId.apply)(_.toUUID)
+  given Meta[SubmitterName]        = Meta[String].timap(SubmitterName.apply)(_.toString)
+  given Meta[TicketContent]        = Meta[String].timap(TicketContent.apply)(_.toString)
+  given Meta[TicketId]             = Meta[Long].timap(TicketId.apply)(_.toLong)
+  given Meta[TicketNumber]         = Meta[Int].timap(TicketNumber.apply)(_.toInt)
+  given Meta[TicketResolution]     = Meta[String].timap(TicketResolution.valueOf)(_.toString)
+  given Meta[TicketStatus]         = Meta[String].timap(TicketStatus.valueOf)(_.toString)
+  given Meta[TicketTitle]          = Meta[String].timap(TicketTitle.apply)(_.toString)
+
+  private val selectTicketColumns =
+    fr"""SELECT
+            "tickets".number AS number,
+            "tickets".title AS title,
+            "tickets".content AS content,
+            "tickets".status AS status,
+            "tickets".resolution AS resolution,
+            "submitters".uid AS submitter_uid,
+            "submitters".name AS submitter_name,
+            "tickets".created_at AS created_at,
+            "tickets".updated_at AS updated_at
+          FROM "tickets"."tickets" AS "tickets"
+          LEFT OUTER JOIN "tickets"."users" AS "submitters"
+          ON "tickets".submitter = "submitters".uid"""
 
   override def allMilestones(projectId: ProjectId): Stream[F, Milestone] =
     sql"""SELECT id, title, description, due_date FROM "tickets"."milestones" WHERE project = $projectId ORDER BY due_date ASC, title ASC"""
@@ -40,6 +73,29 @@
       .stream
       .transact(tx)
 
+  override def allTickets(filter: Option[TicketFilter])(milestoneId: MilestoneId): Stream[F, Ticket] = {
+    val milestoneFilter =
+      fr""""tickets".id IN (SELECT ticket FROM "tickets".milestone_tickets AS "milestone_tickets" WHERE milestone = $milestoneId)"""
+    val tickets = filter match {
+      case None => selectTicketColumns ++ whereAnd(milestoneFilter)
+      case Some(filter) =>
+        val numberFilter = filter.number.toNel.map(numbers => Fragments.in(fr""""tickets".number""", numbers))
+        val statusFilter = filter.status.toNel.map(status => Fragments.in(fr""""tickets".status""", status))
+        val resolutionFilter =
+          filter.resolution.toNel.map(resolutions => Fragments.in(fr""""tickets".resolution""", resolutions))
+        val submitterFilter =
+          filter.submitter.toNel.map(submitters => Fragments.in(fr""""submitters".name""", submitters))
+        selectTicketColumns ++ whereAndOpt(
+          milestoneFilter.some,
+          numberFilter,
+          statusFilter,
+          resolutionFilter,
+          submitterFilter
+        )
+    }
+    tickets.query[Ticket].stream.transact(tx)
+  }
+
   override def createMilestone(projectId: ProjectId)(milestone: Milestone): F[Int] =
     sql"""INSERT INTO "tickets"."milestones"
           (
diff -rN -u old-smederee/modules/tickets/src/main/scala/de/smederee/tickets/MilestoneRepository.scala new-smederee/modules/tickets/src/main/scala/de/smederee/tickets/MilestoneRepository.scala
--- old-smederee/modules/tickets/src/main/scala/de/smederee/tickets/MilestoneRepository.scala	2025-01-15 09:32:36.548509128 +0000
+++ new-smederee/modules/tickets/src/main/scala/de/smederee/tickets/MilestoneRepository.scala	2025-01-15 09:32:36.552509135 +0000
@@ -35,6 +35,17 @@
     */
   def allMilestones(projectId: ProjectId): Stream[F, Milestone]
 
+  /** Return all tickets associated with the given milestone.
+    *
+    * @param filter
+    *   A ticket filter containing possible values which will be used to filter the list of tickets.
+    * @param milestoneId
+    *   The unique internal ID of a milestone for which all tickets shall be returned.
+    * @return
+    *   A stream of tickets associated with the vcs repository which may be empty.
+    */
+  def allTickets(filter: Option[TicketFilter])(milestoneId: MilestoneId): Stream[F, Ticket]
+
   /** Create a database entry for the given milestone definition.
     *
     * @param projectId
diff -rN -u old-smederee/modules/tickets/src/test/scala/de/smederee/tickets/DoobieMilestoneRepositoryTest.scala new-smederee/modules/tickets/src/test/scala/de/smederee/tickets/DoobieMilestoneRepositoryTest.scala
--- old-smederee/modules/tickets/src/test/scala/de/smederee/tickets/DoobieMilestoneRepositoryTest.scala	2025-01-15 09:32:36.548509128 +0000
+++ new-smederee/modules/tickets/src/test/scala/de/smederee/tickets/DoobieMilestoneRepositoryTest.scala	2025-01-15 09:32:36.552509135 +0000
@@ -102,6 +102,54 @@
     }
   }
 
+  test("allTickets must return all tickets associated with the milestone".tag(NeedsDatabase)) {
+    (genProjectOwner.sample, genProject.sample, genMilestone.sample, genTickets.sample) match {
+      case (Some(owner), Some(generatedProject), Some(milestone), Some(rawTickets)) =>
+        val project  = generatedProject.copy(owner = owner)
+        val tickets  = rawTickets.map(_.copy(submitter = None))
+        val dbConfig = configuration.database
+        val tx = Transactor.fromDriverManager[IO](
+          driver = dbConfig.driver,
+          url = dbConfig.url,
+          user = dbConfig.user,
+          password = dbConfig.pass,
+          logHandler = None
+        )
+        val milestoneRepo = new DoobieMilestoneRepository[IO](tx)
+        val ticketRepo    = new DoobieTicketRepository[IO](tx)
+        val test = for {
+          _      <- createProjectOwner(owner)
+          _      <- createTicketsProject(project)
+          repoId <- loadProjectId(owner.uid, project.name)
+          foundTickets <- repoId match {
+            case None => IO.pure(List.empty)
+            case Some(projectId) =>
+              for {
+                _                <- milestoneRepo.createMilestone(projectId)(milestone)
+                _                <- tickets.traverse(ticket => ticketRepo.createTicket(projectId)(ticket))
+                createdMilestone <- milestoneRepo.findMilestone(projectId)(milestone.title)
+                _ <- createdMilestone match {
+                  case None => IO.pure(List.empty)
+                  case Some(milestone) =>
+                    tickets.traverse(ticket => ticketRepo.addMilestone(projectId)(ticket.number)(milestone))
+                }
+                foundTickets <- createdMilestone.map(_.id).getOrElse(None) match {
+                  case None              => IO.pure(List.empty)
+                  case Some(milestoneId) => milestoneRepo.allTickets(None)(milestoneId).compile.toList
+                }
+              } yield foundTickets
+          }
+        } yield foundTickets
+        test.map { foundTickets =>
+          assertEquals(foundTickets.size, tickets.size, "Different number of tickets!")
+          foundTickets.sortBy(_.number).zip(tickets.sortBy(_.number)).map { (found, expected) =>
+            assertEquals(found.copy(createdAt = expected.createdAt, updatedAt = expected.updatedAt), expected)
+          }
+        }
+      case _ => fail("Could not generate data samples!")
+    }
+  }
+
   test("createMilestone must create the milestone".tag(NeedsDatabase)) {
     (genProjectOwner.sample, genProject.sample, genMilestone.sample) match {
       case (Some(owner), Some(generatedProject), Some(milestone)) =>