mirror of
https://github.com/TheAnachronism/docspell.git
synced 2025-06-22 02:18:26 +00:00
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:
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
}
|
@ -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))
|
||||
})
|
||||
|
||||
}
|
||||
|
Reference in New Issue
Block a user