More updates, cranking out group db
This commit is contained in:
parent
fcc774390d
commit
ab16e3fdf0
6 changed files with 136 additions and 21 deletions
|
@ -1,5 +1,7 @@
|
|||
package gs.smolban.db
|
||||
|
||||
import gs.slug.v0.Slug
|
||||
import gs.smolban.model.Group
|
||||
import gs.smolban.model.SmolbanError
|
||||
import gs.smolban.model.Ticket
|
||||
|
||||
|
@ -17,4 +19,9 @@ object DbError:
|
|||
*/
|
||||
case class TicketAlreadyExists(ref: Ticket.Reference) extends DbError
|
||||
|
||||
case class GroupAlreadyExists(
|
||||
id: Group.Id,
|
||||
slug: Slug
|
||||
) extends DbError
|
||||
|
||||
end DbError
|
||||
|
|
|
@ -3,9 +3,33 @@ package gs.smolban.db
|
|||
import cats.data.EitherT
|
||||
import gs.smolban.model.Group
|
||||
|
||||
/** Database interface for [[Group]].
|
||||
*/
|
||||
trait GroupDb[F[_]]:
|
||||
/** Create a new [[Group]] in the database.
|
||||
*
|
||||
* @param group
|
||||
* The [[Group]] to store.
|
||||
* @return
|
||||
* The [[Group]] that was stored, or an error.
|
||||
*/
|
||||
def createGroup(group: Group): EitherT[F, DbError, Group]
|
||||
|
||||
/** Read the specified [[Group]] from the database.
|
||||
*
|
||||
* @param id
|
||||
* The unique identifier of the [[Group]] to retrieve.
|
||||
* @return
|
||||
* The [[Group]], or `None` if the specified ID does not exist.
|
||||
*/
|
||||
def readGroup(id: Group.Id): F[Option[Group]]
|
||||
|
||||
/** Delete the specified [[Group]]. Note that this operation deletes all
|
||||
* [[Ticket]] associated with the group and should be used with care.
|
||||
*
|
||||
* @param id
|
||||
* The unique identifier of the [[Group]] to delete.
|
||||
* @return
|
||||
* `true` if the group was deleted, `false` otherwise.
|
||||
*/
|
||||
def deleteGroup(id: Group.Id): F[Boolean]
|
||||
|
|
|
@ -3,6 +3,8 @@ package gs.smolban.db
|
|||
import cats.data.EitherT
|
||||
import gs.smolban.model.Ticket
|
||||
|
||||
/** Database interface for [[Ticket]].
|
||||
*/
|
||||
trait TicketDb[F[_]]:
|
||||
/** Create a new [[Ticket]] in the database.
|
||||
*
|
||||
|
|
|
@ -0,0 +1,90 @@
|
|||
package gs.smolban.db.doobie
|
||||
|
||||
import cats.MonadError
|
||||
import cats.data.EitherT
|
||||
import cats.syntax.all.*
|
||||
import doobie.*
|
||||
import doobie.implicits.*
|
||||
import gs.smolban.db.DbError
|
||||
import gs.smolban.db.GroupDb
|
||||
import gs.smolban.db.doobie.DoobieTypes.*
|
||||
import gs.smolban.model.CreatedAt
|
||||
import gs.smolban.model.Group
|
||||
|
||||
final class DoobieGroupDb() extends GroupDb[ConnectionIO]:
|
||||
|
||||
import DoobieGroupDb.ErrorCodes
|
||||
import DoobieGroupDb.Sql
|
||||
|
||||
/** @inheritdoc
|
||||
*/
|
||||
override def createGroup(
|
||||
group: Group
|
||||
): EitherT[ConnectionIO, DbError, Group] =
|
||||
EitherT(
|
||||
Sql.createGroup(group).run.attemptSql.flatMap {
|
||||
case Left(ex) =>
|
||||
if ErrorCodes.UniqueConstraintFailed.contains(ex.getErrorCode()) then
|
||||
Left(
|
||||
DbError.GroupAlreadyExists(group.id, group.slug)
|
||||
).pure[ConnectionIO]
|
||||
else MonadError[ConnectionIO, Throwable].raiseError(ex)
|
||||
case Right(_) =>
|
||||
Right(group).pure[ConnectionIO]
|
||||
}
|
||||
)
|
||||
|
||||
/** @inheritdoc
|
||||
*/
|
||||
override def readGroup(id: Group.Id): ConnectionIO[Option[Group]] =
|
||||
Sql.readGroup(id).option
|
||||
|
||||
/** @inheritdoc
|
||||
*/
|
||||
override def deleteGroup(id: Group.Id): ConnectionIO[Boolean] =
|
||||
for
|
||||
_ <- Sql.deleteAllTicketsForGroup(id).run
|
||||
rows <- Sql.deleteGroup(id).run
|
||||
yield rows > 0
|
||||
|
||||
object DoobieGroupDb:
|
||||
|
||||
private object Sql:
|
||||
|
||||
def createGroup(group: Group): Update0 = sql"""
|
||||
INSERT INTO groups (group_id, slug, created_at)
|
||||
VALUES (${group.id}, ${group.slug}, ${group.createdAt})
|
||||
""".update
|
||||
|
||||
def readGroup(groupId: Group.Id): Query0[Group] = sql"""
|
||||
SELECT slug, created_at
|
||||
FROM groups
|
||||
WHERE group_id = $groupId
|
||||
""".query[Group]
|
||||
|
||||
def deleteAllTicketsForGroup(groupId: Group.Id): Update0 = sql"""
|
||||
DELETE FROM tickets
|
||||
WHERE group_id = $groupId
|
||||
""".update
|
||||
|
||||
def deleteGroup(groupId: Group.Id): Update0 = sql"""
|
||||
DELETE FROM groups
|
||||
WHERE group_id = $groupId
|
||||
""".update
|
||||
|
||||
end Sql
|
||||
|
||||
object ErrorCodes:
|
||||
|
||||
val UniqueConstraintFailed: List[Int] =
|
||||
List(Sqlite.UniqueConstraintFailed)
|
||||
|
||||
object Sqlite:
|
||||
|
||||
val UniqueConstraintFailed: Int = 2067
|
||||
|
||||
end Sqlite
|
||||
|
||||
end ErrorCodes
|
||||
|
||||
end DoobieGroupDb
|
|
@ -8,10 +8,8 @@ import gs.smolban.db.DbError
|
|||
import gs.smolban.db.TicketDb
|
||||
import gs.smolban.db.doobie.DoobieTypes.*
|
||||
import gs.smolban.model.CreatedAt
|
||||
import gs.smolban.model.CreatedBy
|
||||
import gs.smolban.model.Group
|
||||
import gs.smolban.model.Ticket
|
||||
import gs.smolban.model.users.User
|
||||
|
||||
final class DoobieTicketDb[F[_]: Async](
|
||||
val xa: Transactor[F]
|
||||
|
@ -46,11 +44,9 @@ object DoobieTicketDb:
|
|||
|
||||
case class TicketContents(
|
||||
createdAt: CreatedAt,
|
||||
createdBy: CreatedBy,
|
||||
title: String,
|
||||
description: String,
|
||||
status: Ticket.Status,
|
||||
assignee: Option[User.Id]
|
||||
status: Ticket.Status
|
||||
)
|
||||
|
||||
// TODO: figure out how to read tags
|
||||
|
|
|
@ -2,11 +2,10 @@ package gs.smolban.db.doobie
|
|||
|
||||
import DoobieTypes.ErrorMessages
|
||||
import doobie.*
|
||||
import gs.slug.v0.Slug
|
||||
import gs.smolban.model.CreatedAt
|
||||
import gs.smolban.model.CreatedBy
|
||||
import gs.smolban.model.Group
|
||||
import gs.smolban.model.Ticket
|
||||
import gs.smolban.model.users.User
|
||||
import gs.uuid.v0.UUID
|
||||
import gs.uuid.v0.UUIDFormat
|
||||
|
||||
|
@ -34,27 +33,21 @@ trait DoobieTypes:
|
|||
implicit val groupIdPut: Put[Group.Id] =
|
||||
Put[Array[Byte]].tcontramap(id => UUIDFormat.toBytes(id.toUUID().toUUID()))
|
||||
|
||||
implicit val userIdGet: Get[User.Id] =
|
||||
Get[Array[Byte]].tmap(bytes => User.Id(UUID(UUIDFormat.fromBytes(bytes))))
|
||||
|
||||
implicit val userIdPut: Put[User.Id] =
|
||||
Put[Array[Byte]].tcontramap(id => UUIDFormat.toBytes(id.toUUID().toUUID()))
|
||||
|
||||
implicit val createdAtGet: Get[CreatedAt] =
|
||||
Get[Long].tmap(CreatedAt.fromMilliseconds)
|
||||
|
||||
implicit val createdAtPut: Put[CreatedAt] =
|
||||
Put[Long].tcontramap(_.toMilliseconds())
|
||||
|
||||
implicit val createdByGet: Get[CreatedBy] =
|
||||
Get[Array[Byte]].tmap(bytes =>
|
||||
CreatedBy(User.Id(UUID(UUIDFormat.fromBytes(bytes))))
|
||||
)
|
||||
implicit val slugGet: Get[Slug] =
|
||||
Get[String].temap { slug =>
|
||||
Slug.validate(slug) match
|
||||
case None => Left(ErrorMessages.invalidSlug(slug))
|
||||
case Some(s) => Right(s)
|
||||
}
|
||||
|
||||
implicit val createdByPut: Put[CreatedBy] =
|
||||
Put[Array[Byte]].tcontramap(id =>
|
||||
UUIDFormat.toBytes(id.toUserId().toUUID().toUUID())
|
||||
)
|
||||
implicit val slugPut: Put[Slug] =
|
||||
Put[String].tcontramap(_.str())
|
||||
|
||||
object DoobieTypes extends DoobieTypes:
|
||||
|
||||
|
@ -63,6 +56,9 @@ object DoobieTypes extends DoobieTypes:
|
|||
def invalidTicketStatus(candidate: String): String =
|
||||
s"'$candidate' is not a valid ticket status."
|
||||
|
||||
def invalidSlug(candidate: String): String =
|
||||
s"'$candidate' is not a valid Group slug."
|
||||
|
||||
end ErrorMessages
|
||||
|
||||
end DoobieTypes
|
||||
|
|
Loading…
Add table
Reference in a new issue