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
|
package gs.smolban.db
|
||||||
|
|
||||||
|
import gs.slug.v0.Slug
|
||||||
|
import gs.smolban.model.Group
|
||||||
import gs.smolban.model.SmolbanError
|
import gs.smolban.model.SmolbanError
|
||||||
import gs.smolban.model.Ticket
|
import gs.smolban.model.Ticket
|
||||||
|
|
||||||
|
@ -17,4 +19,9 @@ object DbError:
|
||||||
*/
|
*/
|
||||||
case class TicketAlreadyExists(ref: Ticket.Reference) extends DbError
|
case class TicketAlreadyExists(ref: Ticket.Reference) extends DbError
|
||||||
|
|
||||||
|
case class GroupAlreadyExists(
|
||||||
|
id: Group.Id,
|
||||||
|
slug: Slug
|
||||||
|
) extends DbError
|
||||||
|
|
||||||
end DbError
|
end DbError
|
||||||
|
|
|
@ -3,9 +3,33 @@ package gs.smolban.db
|
||||||
import cats.data.EitherT
|
import cats.data.EitherT
|
||||||
import gs.smolban.model.Group
|
import gs.smolban.model.Group
|
||||||
|
|
||||||
|
/** Database interface for [[Group]].
|
||||||
|
*/
|
||||||
trait GroupDb[F[_]]:
|
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]
|
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]]
|
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]
|
def deleteGroup(id: Group.Id): F[Boolean]
|
||||||
|
|
|
@ -3,6 +3,8 @@ package gs.smolban.db
|
||||||
import cats.data.EitherT
|
import cats.data.EitherT
|
||||||
import gs.smolban.model.Ticket
|
import gs.smolban.model.Ticket
|
||||||
|
|
||||||
|
/** Database interface for [[Ticket]].
|
||||||
|
*/
|
||||||
trait TicketDb[F[_]]:
|
trait TicketDb[F[_]]:
|
||||||
/** Create a new [[Ticket]] in the database.
|
/** 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.TicketDb
|
||||||
import gs.smolban.db.doobie.DoobieTypes.*
|
import gs.smolban.db.doobie.DoobieTypes.*
|
||||||
import gs.smolban.model.CreatedAt
|
import gs.smolban.model.CreatedAt
|
||||||
import gs.smolban.model.CreatedBy
|
|
||||||
import gs.smolban.model.Group
|
import gs.smolban.model.Group
|
||||||
import gs.smolban.model.Ticket
|
import gs.smolban.model.Ticket
|
||||||
import gs.smolban.model.users.User
|
|
||||||
|
|
||||||
final class DoobieTicketDb[F[_]: Async](
|
final class DoobieTicketDb[F[_]: Async](
|
||||||
val xa: Transactor[F]
|
val xa: Transactor[F]
|
||||||
|
@ -46,11 +44,9 @@ object DoobieTicketDb:
|
||||||
|
|
||||||
case class TicketContents(
|
case class TicketContents(
|
||||||
createdAt: CreatedAt,
|
createdAt: CreatedAt,
|
||||||
createdBy: CreatedBy,
|
|
||||||
title: String,
|
title: String,
|
||||||
description: String,
|
description: String,
|
||||||
status: Ticket.Status,
|
status: Ticket.Status
|
||||||
assignee: Option[User.Id]
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// TODO: figure out how to read tags
|
// TODO: figure out how to read tags
|
||||||
|
|
|
@ -2,11 +2,10 @@ package gs.smolban.db.doobie
|
||||||
|
|
||||||
import DoobieTypes.ErrorMessages
|
import DoobieTypes.ErrorMessages
|
||||||
import doobie.*
|
import doobie.*
|
||||||
|
import gs.slug.v0.Slug
|
||||||
import gs.smolban.model.CreatedAt
|
import gs.smolban.model.CreatedAt
|
||||||
import gs.smolban.model.CreatedBy
|
|
||||||
import gs.smolban.model.Group
|
import gs.smolban.model.Group
|
||||||
import gs.smolban.model.Ticket
|
import gs.smolban.model.Ticket
|
||||||
import gs.smolban.model.users.User
|
|
||||||
import gs.uuid.v0.UUID
|
import gs.uuid.v0.UUID
|
||||||
import gs.uuid.v0.UUIDFormat
|
import gs.uuid.v0.UUIDFormat
|
||||||
|
|
||||||
|
@ -34,27 +33,21 @@ trait DoobieTypes:
|
||||||
implicit val groupIdPut: Put[Group.Id] =
|
implicit val groupIdPut: Put[Group.Id] =
|
||||||
Put[Array[Byte]].tcontramap(id => UUIDFormat.toBytes(id.toUUID().toUUID()))
|
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] =
|
implicit val createdAtGet: Get[CreatedAt] =
|
||||||
Get[Long].tmap(CreatedAt.fromMilliseconds)
|
Get[Long].tmap(CreatedAt.fromMilliseconds)
|
||||||
|
|
||||||
implicit val createdAtPut: Put[CreatedAt] =
|
implicit val createdAtPut: Put[CreatedAt] =
|
||||||
Put[Long].tcontramap(_.toMilliseconds())
|
Put[Long].tcontramap(_.toMilliseconds())
|
||||||
|
|
||||||
implicit val createdByGet: Get[CreatedBy] =
|
implicit val slugGet: Get[Slug] =
|
||||||
Get[Array[Byte]].tmap(bytes =>
|
Get[String].temap { slug =>
|
||||||
CreatedBy(User.Id(UUID(UUIDFormat.fromBytes(bytes))))
|
Slug.validate(slug) match
|
||||||
)
|
case None => Left(ErrorMessages.invalidSlug(slug))
|
||||||
|
case Some(s) => Right(s)
|
||||||
|
}
|
||||||
|
|
||||||
implicit val createdByPut: Put[CreatedBy] =
|
implicit val slugPut: Put[Slug] =
|
||||||
Put[Array[Byte]].tcontramap(id =>
|
Put[String].tcontramap(_.str())
|
||||||
UUIDFormat.toBytes(id.toUserId().toUUID().toUUID())
|
|
||||||
)
|
|
||||||
|
|
||||||
object DoobieTypes extends DoobieTypes:
|
object DoobieTypes extends DoobieTypes:
|
||||||
|
|
||||||
|
@ -63,6 +56,9 @@ object DoobieTypes extends DoobieTypes:
|
||||||
def invalidTicketStatus(candidate: String): String =
|
def invalidTicketStatus(candidate: String): String =
|
||||||
s"'$candidate' is not a valid ticket status."
|
s"'$candidate' is not a valid ticket status."
|
||||||
|
|
||||||
|
def invalidSlug(candidate: String): String =
|
||||||
|
s"'$candidate' is not a valid Group slug."
|
||||||
|
|
||||||
end ErrorMessages
|
end ErrorMessages
|
||||||
|
|
||||||
end DoobieTypes
|
end DoobieTypes
|
||||||
|
|
Loading…
Add table
Reference in a new issue