Implement space operations

This commit is contained in:
Eike Kettner
2020-07-08 00:21:48 +02:00
parent 0e8c9b1819
commit 752a94a9e2
11 changed files with 358 additions and 74 deletions

View File

@ -0,0 +1,29 @@
package docspell.store
import cats.implicits._
import cats.ApplicativeError
sealed trait UpdateResult
object UpdateResult {
case object Success extends UpdateResult
case object NotFound extends UpdateResult
final case class Failure(ex: Throwable) extends UpdateResult
def success: UpdateResult = Success
def notFound: UpdateResult = NotFound
def failure(ex: Throwable): UpdateResult = Failure(ex)
def fromUpdateRows(n: Int): UpdateResult =
if (n > 0) success
else notFound
def fromUpdate[F[_]](
fn: F[Int]
)(implicit ev: ApplicativeError[F, Throwable]): F[UpdateResult] =
fn.attempt.map {
case Right(n) => fromUpdateRows(n)
case Left(ex) => failure(ex)
}
}

View File

@ -0,0 +1,188 @@
package docspell.store.queries
import cats.data.OptionT
import cats.implicits._
import docspell.common._
import docspell.store.impl.Implicits._
import docspell.store.records._
import doobie._
import doobie.implicits._
object QSpace {
final case class SpaceItem(
id: Ident,
name: String,
owner: IdRef,
created: Timestamp
) {
def withMembers(members: List[IdRef]): SpaceDetail =
SpaceDetail(id, name, owner, created, members)
}
final case class SpaceDetail(
id: Ident,
name: String,
owner: IdRef,
created: Timestamp,
members: List[IdRef]
)
sealed trait SpaceChangeResult
object SpaceChangeResult {
case object Success extends SpaceChangeResult
def success: SpaceChangeResult = Success
case object NotFound extends SpaceChangeResult
def notFound: SpaceChangeResult = NotFound
case object Forbidden extends SpaceChangeResult
def forbidden: SpaceChangeResult = Forbidden
case object Exists extends SpaceChangeResult
def exists: SpaceChangeResult = Exists
}
def delete(id: Ident, account: AccountId): ConnectionIO[SpaceChangeResult] = {
def tryDelete =
for {
_ <- RItem.removeSpace(id)
_ <- RSpaceMember.deleteAll(id)
_ <- RSpace.delete(id)
} yield SpaceChangeResult.success
(for {
uid <- OptionT(findUserId(account))
space <- OptionT(RSpace.findById(id))
res <- OptionT.liftF(
if (space.owner == uid) tryDelete
else SpaceChangeResult.forbidden.pure[ConnectionIO]
)
} yield res).getOrElse(SpaceChangeResult.notFound)
}
def changeName(
space: Ident,
account: AccountId,
name: String
): ConnectionIO[SpaceChangeResult] = {
def tryUpdate(ns: RSpace): ConnectionIO[SpaceChangeResult] =
for {
n <- RSpace.update(ns)
res =
if (n == 0) SpaceChangeResult.notFound
else SpaceChangeResult.Success
} yield res
(for {
uid <- OptionT(findUserId(account))
space <- OptionT(RSpace.findById(space))
res <- OptionT.liftF(
if (space.owner == uid) tryUpdate(space.copy(name = name))
else SpaceChangeResult.forbidden.pure[ConnectionIO]
)
} yield res).getOrElse(SpaceChangeResult.notFound)
}
def removeMember(
space: Ident,
account: AccountId,
member: Ident
): ConnectionIO[SpaceChangeResult] = {
def tryRemove: ConnectionIO[SpaceChangeResult] =
for {
n <- RSpaceMember.delete(member, space)
res =
if (n == 0) SpaceChangeResult.notFound
else SpaceChangeResult.Success
} yield res
(for {
uid <- OptionT(findUserId(account))
space <- OptionT(RSpace.findById(space))
res <- OptionT.liftF(
if (space.owner == uid) tryRemove
else SpaceChangeResult.forbidden.pure[ConnectionIO]
)
} yield res).getOrElse(SpaceChangeResult.notFound)
}
def addMember(
space: Ident,
account: AccountId,
member: Ident
): ConnectionIO[SpaceChangeResult] = {
def tryAdd: ConnectionIO[SpaceChangeResult] =
for {
spm <- RSpaceMember.findByUserId(member, space)
mem <- RSpaceMember.newMember[ConnectionIO](space, member)
res <-
if (spm.isDefined) SpaceChangeResult.exists.pure[ConnectionIO]
else RSpaceMember.insert(mem).map(_ => SpaceChangeResult.Success)
} yield res
(for {
uid <- OptionT(findUserId(account))
space <- OptionT(RSpace.findById(space))
res <- OptionT.liftF(
if (space.owner == uid) tryAdd
else SpaceChangeResult.forbidden.pure[ConnectionIO]
)
} yield res).getOrElse(SpaceChangeResult.notFound)
}
def findById(id: Ident, collective: Ident): ConnectionIO[Option[SpaceDetail]] = {
val mUserId = RSpaceMember.Columns.user.prefix("m")
val mSpaceId = RSpaceMember.Columns.space.prefix("m")
val uId = RUser.Columns.uid.prefix("u")
val uLogin = RUser.Columns.login.prefix("u")
val sColl = RSpace.Columns.collective.prefix("s")
val sId = RSpace.Columns.id.prefix("s")
val from = RSpaceMember.table ++ fr"m INNER JOIN" ++
RUser.table ++ fr"u ON" ++ mUserId.is(uId) ++ fr"INNER JOIN" ++
RSpace.table ++ fr"s ON" ++ mSpaceId.is(sId)
val memberQ = selectSimple(
Seq(uId, uLogin),
from,
and(mSpaceId.is(id), sColl.is(collective))
).query[IdRef].to[Vector]
(for {
space <- OptionT(findAll(collective, Some(id), None).map(_.headOption))
memb <- OptionT.liftF(memberQ)
} yield space.withMembers(memb.toList)).value
}
def findAll(
collective: Ident,
idQ: Option[Ident],
nameQ: Option[String]
): ConnectionIO[Vector[SpaceItem]] = {
val uId = RUser.Columns.uid.prefix("u")
val sId = RSpace.Columns.id.prefix("s")
val sOwner = RSpace.Columns.owner.prefix("s")
val sName = RSpace.Columns.name.prefix("s")
val sColl = RSpace.Columns.collective.prefix("s")
val cols = Seq(
sId,
sName,
uId,
RUser.Columns.login.prefix("u"),
RSpace.Columns.created.prefix("s")
)
val from = RSpace.table ++ fr"s INNER JOIN" ++
RUser.table ++ fr"u ON" ++ uId.is(sOwner)
val where =
sColl.is(collective) :: idQ.toList.map(id => sId.is(id)) ::: nameQ.toList.map(q =>
sName.lowerLike(s"%${q.toLowerCase}%")
)
selectSimple(cols, from, and(where) ++ orderBy(sName.asc)).query[SpaceItem].to[Vector]
}
private def findUserId(account: AccountId): ConnectionIO[Option[Ident]] =
RUser.findByAccount(account).map(_.map(_.uid))
}

View File

@ -299,4 +299,9 @@ object RItem {
def findByIdAndCollective(itemId: Ident, coll: Ident): ConnectionIO[Option[RItem]] =
selectSimple(all, table, and(id.is(itemId), cid.is(coll))).query[RItem].option
def removeSpace(spaceId: Ident): ConnectionIO[Int] = {
val empty: Option[Ident] = None
updateRow(table, space.is(spaceId), space.setTo(empty)).update.run
}
}

View File

@ -80,4 +80,6 @@ object RSpace {
sql.query[RSpace].to[Vector]
}
def delete(spaceId: Ident): ConnectionIO[Int] =
deleteFrom(table, id.is(spaceId)).update.run
}

View File

@ -1,5 +1,8 @@
package docspell.store.records
import cats.effect._
import cats.implicits._
import docspell.common._
import docspell.store.impl.Column
import docspell.store.impl.Implicits._
@ -16,7 +19,13 @@ case class RSpaceMember(
object RSpaceMember {
val table = fr"space"
def newMember[F[_]: Sync](space: Ident, user: Ident): F[RSpaceMember] =
for {
nId <- Ident.randomId[F]
now <- Timestamp.current[F]
} yield RSpaceMember(nId, space, user, now)
val table = fr"space_member"
object Columns {
@ -39,4 +48,14 @@ object RSpaceMember {
sql.update.run
}
def findByUserId(userId: Ident, spaceId: Ident): ConnectionIO[Option[RSpaceMember]] =
selectSimple(all, table, and(space.is(spaceId), user.is(userId)))
.query[RSpaceMember]
.option
def delete(userId: Ident, spaceId: Ident): ConnectionIO[Int] =
deleteFrom(table, and(space.is(spaceId), user.is(userId))).update.run
def deleteAll(spaceId: Ident): ConnectionIO[Int] =
deleteFrom(table, space.is(spaceId)).update.run
}