Refactor user tasks to support collective and user scopes

Before, there were periodic tasks run per collective and not user by
making sure that submitter + group are the same value. This is now
encoded in `UserTaskScope` so it is now obvious and errors can be
reduced when using this.
This commit is contained in:
eikek
2021-08-14 21:10:09 +02:00
parent 548dfb9a57
commit 31d885ed79
14 changed files with 177 additions and 114 deletions

View File

@ -12,7 +12,7 @@ import docspell.common._
import docspell.store.qb.DSL._
import docspell.store.qb._
import docspell.store.records._
import docspell.store.usertask.UserTask
import docspell.store.usertask.{UserTask, UserTaskScope}
import doobie._
@ -54,15 +54,15 @@ object QUserTask {
)
).query[RPeriodicTask].option.map(_.map(makeUserTask))
def insert(account: AccountId, task: UserTask[String]): ConnectionIO[Int] =
def insert(scope: UserTaskScope, task: UserTask[String]): ConnectionIO[Int] =
for {
r <- task.toPeriodicTask[ConnectionIO](account)
r <- task.toPeriodicTask[ConnectionIO](scope)
n <- RPeriodicTask.insert(r)
} yield n
def update(account: AccountId, task: UserTask[String]): ConnectionIO[Int] =
def update(scope: UserTaskScope, task: UserTask[String]): ConnectionIO[Int] =
for {
r <- task.toPeriodicTask[ConnectionIO](account)
r <- task.toPeriodicTask[ConnectionIO](scope)
n <- RPeriodicTask.update(r)
} yield n

View File

@ -13,6 +13,7 @@ import cats.implicits._
import docspell.common._
import docspell.store.qb.DSL._
import docspell.store.qb._
import docspell.store.usertask.UserTaskScope
import com.github.eikek.calev.CalEvent
import doobie._
@ -67,11 +68,10 @@ object RPeriodicTask {
def create[F[_]: Sync](
enabled: Boolean,
scope: UserTaskScope,
task: Ident,
group: Ident,
args: String,
subject: String,
submitter: Ident,
priority: Priority,
timer: CalEvent,
summary: Option[String]
@ -86,10 +86,10 @@ object RPeriodicTask {
id,
enabled,
task,
group,
scope.collective,
args,
subject,
submitter,
scope.fold(_.user, identity),
priority,
None,
None,
@ -107,22 +107,20 @@ object RPeriodicTask {
def createJson[F[_]: Sync, A](
enabled: Boolean,
scope: UserTaskScope,
task: Ident,
group: Ident,
args: A,
subject: String,
submitter: Ident,
priority: Priority,
timer: CalEvent,
summary: Option[String]
)(implicit E: Encoder[A]): F[RPeriodicTask] =
create[F](
enabled,
scope,
task,
group,
E(args).noSpaces,
subject,
submitter,
priority,
timer,
summary

View File

@ -43,16 +43,15 @@ object UserTask {
.map(a => ut.copy(args = a))
def toPeriodicTask[F[_]: Sync](
account: AccountId
scope: UserTaskScope
): F[RPeriodicTask] =
RPeriodicTask
.create[F](
ut.enabled,
scope,
ut.name,
account.collective,
ut.args,
s"${account.user.id}: ${ut.name.id}",
account.user,
s"${scope.fold(_.user.id, _.id)}: ${ut.name.id}",
Priority.Low,
ut.timer,
ut.summary

View File

@ -0,0 +1,52 @@
/*
* Copyright 2020 Docspell Contributors
*
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package docspell.store.usertask
import docspell.common._
sealed trait UserTaskScope { self: Product =>
def name: String =
productPrefix.toLowerCase
def collective: Ident
def fold[A](fa: AccountId => A, fb: Ident => A): A
/** Maps to the account or uses the collective for both parts if the
* scope is collective wide.
*/
private[usertask] def toAccountId: AccountId =
AccountId(collective, fold(_.user, identity))
}
object UserTaskScope {
final case class Account(account: AccountId) extends UserTaskScope {
val collective = account.collective
def fold[A](fa: AccountId => A, fb: Ident => A): A =
fa(account)
}
final case class Collective(collective: Ident) extends UserTaskScope {
def fold[A](fa: AccountId => A, fb: Ident => A): A =
fb(collective)
}
def collective(id: Ident): UserTaskScope =
Collective(id)
def account(accountId: AccountId): UserTaskScope =
Account(accountId)
def apply(accountId: AccountId): UserTaskScope =
UserTaskScope.account(accountId)
def apply(collective: Ident): UserTaskScope =
UserTaskScope.collective(collective)
}

View File

@ -22,13 +22,15 @@ import io.circe._
* once.
*
* This class defines methods at a higher level, dealing with
* `UserTask` and `AccountId` instead of directly using
* `UserTask` and `UserTaskScope` instead of directly using
* `RPeriodicTask`. A user task is associated to a specific user (not
* just the collective).
* just the collective). But it can be associated to the whole
* collective by using the collective as submitter, too. This is
* abstracted in `UserTaskScope`.
*
* implNote: The mapping is as follows: The collective is the task
* group. The submitter property contains the username. Once a task
* is saved to the database, it can only be refernced uniquely by its
* is saved to the database, it can only be referenced uniquely by its
* id. A user may submit multiple same tasks (with different
* properties).
*/
@ -36,22 +38,22 @@ trait UserTaskStore[F[_]] {
/** Return all tasks of the given user.
*/
def getAll(account: AccountId): Stream[F, UserTask[String]]
def getAll(scope: UserTaskScope): Stream[F, UserTask[String]]
/** Return all tasks of the given name and user. The task's arguments
* are returned as stored in the database.
*/
def getByNameRaw(account: AccountId, name: Ident): Stream[F, UserTask[String]]
def getByNameRaw(scope: UserTaskScope, name: Ident): Stream[F, UserTask[String]]
/** Return all tasks of the given name and user. The task's arguments
* are decoded using the given json decoder.
*/
def getByName[A](account: AccountId, name: Ident)(implicit
def getByName[A](scope: UserTaskScope, name: Ident)(implicit
D: Decoder[A]
): Stream[F, UserTask[A]]
/** Return a user-task with the given id. */
def getByIdRaw(account: AccountId, id: Ident): OptionT[F, UserTask[String]]
def getByIdRaw(scope: UserTaskScope, id: Ident): OptionT[F, UserTask[String]]
/** Updates or inserts the given task.
*
@ -59,23 +61,23 @@ trait UserTaskStore[F[_]] {
* exists, a new one is created. Otherwise the existing task is
* updated.
*/
def updateTask[A](account: AccountId, ut: UserTask[A])(implicit E: Encoder[A]): F[Int]
def updateTask[A](scope: UserTaskScope, ut: UserTask[A])(implicit E: Encoder[A]): F[Int]
/** Delete the task with the given id of the given user.
*/
def deleteTask(account: AccountId, id: Ident): F[Int]
def deleteTask(scope: UserTaskScope, id: Ident): F[Int]
/** Return the task of the given user and name. If multiple exists, an
* error is returned. The task's arguments are returned as stored
* in the database.
*/
def getOneByNameRaw(account: AccountId, name: Ident): OptionT[F, UserTask[String]]
def getOneByNameRaw(scope: UserTaskScope, name: Ident): OptionT[F, UserTask[String]]
/** Return the task of the given user and name. If multiple exists, an
* error is returned. The task's arguments are decoded using the
* given json decoder.
*/
def getOneByName[A](account: AccountId, name: Ident)(implicit
def getOneByName[A](scope: UserTaskScope, name: Ident)(implicit
D: Decoder[A]
): OptionT[F, UserTask[A]]
@ -90,13 +92,13 @@ trait UserTaskStore[F[_]] {
* the user `account`, they will all be removed and the given task
* inserted!
*/
def updateOneTask[A](account: AccountId, ut: UserTask[A])(implicit
def updateOneTask[A](scope: UserTaskScope, ut: UserTask[A])(implicit
E: Encoder[A]
): F[UserTask[String]]
/** Delete all tasks of the given user that have name `name'.
*/
def deleteAll(account: AccountId, name: Ident): F[Int]
def deleteAll(scope: UserTaskScope, name: Ident): F[Int]
}
object UserTaskStore {
@ -104,47 +106,47 @@ object UserTaskStore {
def apply[F[_]: Async](store: Store[F]): Resource[F, UserTaskStore[F]] =
Resource.pure[F, UserTaskStore[F]](new UserTaskStore[F] {
def getAll(account: AccountId): Stream[F, UserTask[String]] =
store.transact(QUserTask.findAll(account))
def getAll(scope: UserTaskScope): Stream[F, UserTask[String]] =
store.transact(QUserTask.findAll(scope.toAccountId))
def getByNameRaw(account: AccountId, name: Ident): Stream[F, UserTask[String]] =
store.transact(QUserTask.findByName(account, name))
def getByNameRaw(scope: UserTaskScope, name: Ident): Stream[F, UserTask[String]] =
store.transact(QUserTask.findByName(scope.toAccountId, name))
def getByIdRaw(account: AccountId, id: Ident): OptionT[F, UserTask[String]] =
OptionT(store.transact(QUserTask.findById(account, id)))
def getByIdRaw(scope: UserTaskScope, id: Ident): OptionT[F, UserTask[String]] =
OptionT(store.transact(QUserTask.findById(scope.toAccountId, id)))
def getByName[A](account: AccountId, name: Ident)(implicit
def getByName[A](scope: UserTaskScope, name: Ident)(implicit
D: Decoder[A]
): Stream[F, UserTask[A]] =
getByNameRaw(account, name).flatMap(_.decode match {
getByNameRaw(scope, name).flatMap(_.decode match {
case Right(ua) => Stream.emit(ua)
case Left(err) => Stream.raiseError[F](new Exception(err))
})
def updateTask[A](account: AccountId, ut: UserTask[A])(implicit
def updateTask[A](scope: UserTaskScope, ut: UserTask[A])(implicit
E: Encoder[A]
): F[Int] = {
val exists = QUserTask.exists(ut.id)
val insert = QUserTask.insert(account, ut.encode)
val insert = QUserTask.insert(scope, ut.encode)
store.add(insert, exists).flatMap {
case AddResult.Success =>
1.pure[F]
case AddResult.EntityExists(_) =>
store.transact(QUserTask.update(account, ut.encode))
store.transact(QUserTask.update(scope, ut.encode))
case AddResult.Failure(ex) =>
Async[F].raiseError(ex)
}
}
def deleteTask(account: AccountId, id: Ident): F[Int] =
store.transact(QUserTask.delete(account, id))
def deleteTask(scope: UserTaskScope, id: Ident): F[Int] =
store.transact(QUserTask.delete(scope.toAccountId, id))
def getOneByNameRaw(
account: AccountId,
scope: UserTaskScope,
name: Ident
): OptionT[F, UserTask[String]] =
OptionT(
getByNameRaw(account, name)
getByNameRaw(scope, name)
.take(2)
.compile
.toList
@ -155,32 +157,34 @@ object UserTaskStore {
}
)
def getOneByName[A](account: AccountId, name: Ident)(implicit
def getOneByName[A](scope: UserTaskScope, name: Ident)(implicit
D: Decoder[A]
): OptionT[F, UserTask[A]] =
getOneByNameRaw(account, name)
getOneByNameRaw(scope, name)
.semiflatMap(_.decode match {
case Right(ua) => ua.pure[F]
case Left(err) => Async[F].raiseError(new Exception(err))
})
def updateOneTask[A](account: AccountId, ut: UserTask[A])(implicit
def updateOneTask[A](scope: UserTaskScope, ut: UserTask[A])(implicit
E: Encoder[A]
): F[UserTask[String]] =
getByNameRaw(account, ut.name).compile.toList.flatMap {
getByNameRaw(scope, ut.name).compile.toList.flatMap {
case a :: rest =>
val task = ut.copy(id = a.id).encode
for {
_ <- store.transact(QUserTask.update(account, task))
_ <- store.transact(rest.traverse(t => QUserTask.delete(account, t.id)))
_ <- store.transact(QUserTask.update(scope, task))
_ <- store.transact(
rest.traverse(t => QUserTask.delete(scope.toAccountId, t.id))
)
} yield task
case Nil =>
val task = ut.encode
store.transact(QUserTask.insert(account, task)).map(_ => task)
store.transact(QUserTask.insert(scope, task)).map(_ => task)
}
def deleteAll(account: AccountId, name: Ident): F[Int] =
store.transact(QUserTask.deleteAll(account, name))
def deleteAll(scope: UserTaskScope, name: Ident): F[Int] =
store.transact(QUserTask.deleteAll(scope.toAccountId, name))
})
}