mirror of
https://github.com/TheAnachronism/docspell.git
synced 2025-06-04 06:05:59 +00:00
Server-side stub impl for notify-due-items
This commit is contained in:
parent
e97e0db45c
commit
ad772c0c25
@ -7,6 +7,7 @@ import docspell.backend.signup.OSignup
|
|||||||
import docspell.store.Store
|
import docspell.store.Store
|
||||||
import docspell.store.ops.ONode
|
import docspell.store.ops.ONode
|
||||||
import docspell.store.queue.JobQueue
|
import docspell.store.queue.JobQueue
|
||||||
|
import docspell.store.usertask.UserTaskStore
|
||||||
|
|
||||||
import scala.concurrent.ExecutionContext
|
import scala.concurrent.ExecutionContext
|
||||||
import emil.javamail.JavaMailEmil
|
import emil.javamail.JavaMailEmil
|
||||||
@ -26,6 +27,7 @@ trait BackendApp[F[_]] {
|
|||||||
def item: OItem[F]
|
def item: OItem[F]
|
||||||
def mail: OMail[F]
|
def mail: OMail[F]
|
||||||
def joex: OJoex[F]
|
def joex: OJoex[F]
|
||||||
|
def userTask: OUserTask[F]
|
||||||
}
|
}
|
||||||
|
|
||||||
object BackendApp {
|
object BackendApp {
|
||||||
@ -37,20 +39,22 @@ object BackendApp {
|
|||||||
blocker: Blocker
|
blocker: Blocker
|
||||||
): Resource[F, BackendApp[F]] =
|
): Resource[F, BackendApp[F]] =
|
||||||
for {
|
for {
|
||||||
queue <- JobQueue(store)
|
utStore <- UserTaskStore(store)
|
||||||
loginImpl <- Login[F](store)
|
queue <- JobQueue(store)
|
||||||
signupImpl <- OSignup[F](store)
|
loginImpl <- Login[F](store)
|
||||||
collImpl <- OCollective[F](store)
|
signupImpl <- OSignup[F](store)
|
||||||
sourceImpl <- OSource[F](store)
|
collImpl <- OCollective[F](store)
|
||||||
tagImpl <- OTag[F](store)
|
sourceImpl <- OSource[F](store)
|
||||||
equipImpl <- OEquipment[F](store)
|
tagImpl <- OTag[F](store)
|
||||||
orgImpl <- OOrganization(store)
|
equipImpl <- OEquipment[F](store)
|
||||||
joexImpl <- OJoex.create(httpClientEc, store)
|
orgImpl <- OOrganization(store)
|
||||||
uploadImpl <- OUpload(store, queue, cfg, joexImpl)
|
joexImpl <- OJoex.create(httpClientEc, store)
|
||||||
nodeImpl <- ONode(store)
|
uploadImpl <- OUpload(store, queue, cfg, joexImpl)
|
||||||
jobImpl <- OJob(store, joexImpl)
|
nodeImpl <- ONode(store)
|
||||||
itemImpl <- OItem(store)
|
jobImpl <- OJob(store, joexImpl)
|
||||||
mailImpl <- OMail(store, JavaMailEmil(blocker))
|
itemImpl <- OItem(store)
|
||||||
|
mailImpl <- OMail(store, JavaMailEmil(blocker))
|
||||||
|
userTaskImpl <- OUserTask(utStore, joexImpl)
|
||||||
} yield new BackendApp[F] {
|
} yield new BackendApp[F] {
|
||||||
val login: Login[F] = loginImpl
|
val login: Login[F] = loginImpl
|
||||||
val signup: OSignup[F] = signupImpl
|
val signup: OSignup[F] = signupImpl
|
||||||
@ -65,6 +69,7 @@ object BackendApp {
|
|||||||
val item = itemImpl
|
val item = itemImpl
|
||||||
val mail = mailImpl
|
val mail = mailImpl
|
||||||
val joex = joexImpl
|
val joex = joexImpl
|
||||||
|
val userTask = userTaskImpl
|
||||||
}
|
}
|
||||||
|
|
||||||
def apply[F[_]: ConcurrentEffect: ContextShift](
|
def apply[F[_]: ConcurrentEffect: ContextShift](
|
||||||
|
@ -0,0 +1,60 @@
|
|||||||
|
package docspell.backend.ops
|
||||||
|
|
||||||
|
import cats.implicits._
|
||||||
|
import cats.effect._
|
||||||
|
import docspell.store.usertask._
|
||||||
|
import docspell.common._
|
||||||
|
import com.github.eikek.calev.CalEvent
|
||||||
|
|
||||||
|
trait OUserTask[F[_]] {
|
||||||
|
|
||||||
|
def getNotifyDueItems(account: AccountId): F[UserTask[NotifyDueItemsArgs]]
|
||||||
|
|
||||||
|
def submitNotifyDueItems(
|
||||||
|
account: AccountId,
|
||||||
|
task: UserTask[NotifyDueItemsArgs]
|
||||||
|
): F[Unit]
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
object OUserTask {
|
||||||
|
|
||||||
|
def apply[F[_]: Effect](store: UserTaskStore[F], joex: OJoex[F]): Resource[F, OUserTask[F]] =
|
||||||
|
Resource.pure[F, OUserTask[F]](new OUserTask[F] {
|
||||||
|
|
||||||
|
def getNotifyDueItems(account: AccountId): F[UserTask[NotifyDueItemsArgs]] =
|
||||||
|
store
|
||||||
|
.getOneByName[NotifyDueItemsArgs](account, NotifyDueItemsArgs.taskName)
|
||||||
|
.getOrElseF(notifyDueItemsDefault(account))
|
||||||
|
|
||||||
|
def submitNotifyDueItems(
|
||||||
|
account: AccountId,
|
||||||
|
task: UserTask[NotifyDueItemsArgs]
|
||||||
|
): F[Unit] =
|
||||||
|
for {
|
||||||
|
_ <- store.updateOneTask[NotifyDueItemsArgs](account, task)
|
||||||
|
_ <- joex.notifyAllNodes
|
||||||
|
} yield ()
|
||||||
|
|
||||||
|
private def notifyDueItemsDefault(
|
||||||
|
account: AccountId
|
||||||
|
): F[UserTask[NotifyDueItemsArgs]] =
|
||||||
|
for {
|
||||||
|
id <- Ident.randomId[F]
|
||||||
|
} yield UserTask(
|
||||||
|
id,
|
||||||
|
NotifyDueItemsArgs.taskName,
|
||||||
|
false,
|
||||||
|
CalEvent.unsafe("*-*-1/7 12:00"),
|
||||||
|
NotifyDueItemsArgs(
|
||||||
|
account,
|
||||||
|
Ident.unsafe("none"),
|
||||||
|
Nil,
|
||||||
|
5,
|
||||||
|
Nil,
|
||||||
|
Nil
|
||||||
|
)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
@ -1,5 +1,7 @@
|
|||||||
package docspell.common
|
package docspell.common
|
||||||
|
|
||||||
|
import io.circe._
|
||||||
|
|
||||||
case class AccountId(collective: Ident, user: Ident) {
|
case class AccountId(collective: Ident, user: Ident) {
|
||||||
|
|
||||||
def asString =
|
def asString =
|
||||||
@ -32,4 +34,9 @@ object AccountId {
|
|||||||
|
|
||||||
separated.orElse(Ident.fromString(str).map(id => AccountId(id, id)))
|
separated.orElse(Ident.fromString(str).map(id => AccountId(id, id)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
implicit val jsonDecoder: Decoder[AccountId] =
|
||||||
|
Decoder.decodeString.emap(parse)
|
||||||
|
implicit val jsonEncoder: Encoder[AccountId] =
|
||||||
|
Encoder.encodeString.contramap(_.asString)
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,35 @@
|
|||||||
|
package docspell.common
|
||||||
|
|
||||||
|
import io.circe._, io.circe.generic.semiauto._
|
||||||
|
import docspell.common.syntax.all._
|
||||||
|
|
||||||
|
/** Arguments to the notification task.
|
||||||
|
*
|
||||||
|
* This tasks queries items with a due date and informs the user via
|
||||||
|
* mail.
|
||||||
|
*
|
||||||
|
* If the structure changes, there must be some database migration to
|
||||||
|
* update or remove the json data of the corresponding task.
|
||||||
|
*/
|
||||||
|
case class NotifyDueItemsArgs(
|
||||||
|
account: AccountId,
|
||||||
|
smtpConnection: Ident,
|
||||||
|
recipients: List[String],
|
||||||
|
remindDays: Int,
|
||||||
|
tagsInclude: List[Ident],
|
||||||
|
tagsExclude: List[Ident]
|
||||||
|
) {}
|
||||||
|
|
||||||
|
object NotifyDueItemsArgs {
|
||||||
|
|
||||||
|
val taskName = Ident.unsafe("notify-due-items")
|
||||||
|
|
||||||
|
implicit val jsonEncoder: Encoder[NotifyDueItemsArgs] =
|
||||||
|
deriveEncoder[NotifyDueItemsArgs]
|
||||||
|
implicit val jsonDecoder: Decoder[NotifyDueItemsArgs] =
|
||||||
|
deriveDecoder[NotifyDueItemsArgs]
|
||||||
|
|
||||||
|
def parse(str: String): Either[Throwable, NotifyDueItemsArgs] =
|
||||||
|
str.parseJsonAs[NotifyDueItemsArgs]
|
||||||
|
|
||||||
|
}
|
@ -2,7 +2,7 @@ package docspell.joex
|
|||||||
|
|
||||||
import cats.implicits._
|
import cats.implicits._
|
||||||
import cats.effect._
|
import cats.effect._
|
||||||
import docspell.common.{Ident, NodeType, ProcessItemArgs}
|
import docspell.common._
|
||||||
import docspell.joex.hk._
|
import docspell.joex.hk._
|
||||||
import docspell.joex.process.ItemHandler
|
import docspell.joex.process.ItemHandler
|
||||||
import docspell.joex.scheduler._
|
import docspell.joex.scheduler._
|
||||||
@ -75,6 +75,13 @@ object JoexAppImpl {
|
|||||||
ItemHandler.onCancel[F]
|
ItemHandler.onCancel[F]
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
.withTask(
|
||||||
|
JobTask.json(
|
||||||
|
NotifyDueItemsArgs.taskName,
|
||||||
|
NotifyDueItemsTask[F],
|
||||||
|
NotifyDueItemsTask.onCancel[F]
|
||||||
|
)
|
||||||
|
)
|
||||||
.withTask(
|
.withTask(
|
||||||
JobTask.json(
|
JobTask.json(
|
||||||
HouseKeepingTask.taskName,
|
HouseKeepingTask.taskName,
|
||||||
|
@ -17,12 +17,12 @@ object HouseKeepingTask {
|
|||||||
|
|
||||||
def apply[F[_]: Sync](cfg: Config): Task[F, Unit, Unit] =
|
def apply[F[_]: Sync](cfg: Config): Task[F, Unit, Unit] =
|
||||||
Task
|
Task
|
||||||
.log[F](_.info(s"Running house-keeping task now"))
|
.log[F, Unit](_.info(s"Running house-keeping task now"))
|
||||||
.flatMap(_ => CleanupInvitesTask(cfg.houseKeeping.cleanupInvites))
|
.flatMap(_ => CleanupInvitesTask(cfg.houseKeeping.cleanupInvites))
|
||||||
.flatMap(_ => CleanupJobsTask(cfg.houseKeeping.cleanupJobs))
|
.flatMap(_ => CleanupJobsTask(cfg.houseKeeping.cleanupJobs))
|
||||||
|
|
||||||
def onCancel[F[_]: Sync]: Task[F, Unit, Unit] =
|
def onCancel[F[_]: Sync]: Task[F, Unit, Unit] =
|
||||||
Task.log(_.warn("Cancelling house-keeping task"))
|
Task.log[F, Unit](_.warn("Cancelling house-keeping task"))
|
||||||
|
|
||||||
def periodicTask[F[_]: Sync](ce: CalEvent): F[RPeriodicTask] =
|
def periodicTask[F[_]: Sync](ce: CalEvent): F[RPeriodicTask] =
|
||||||
RPeriodicTask
|
RPeriodicTask
|
||||||
|
@ -0,0 +1,23 @@
|
|||||||
|
package docspell.joex.hk
|
||||||
|
|
||||||
|
import cats.implicits._
|
||||||
|
import cats.effect._
|
||||||
|
|
||||||
|
import docspell.common._
|
||||||
|
import docspell.joex.scheduler.Task
|
||||||
|
|
||||||
|
object NotifyDueItemsTask {
|
||||||
|
|
||||||
|
def apply[F[_]: Sync](): Task[F, NotifyDueItemsArgs, Unit] =
|
||||||
|
Task { ctx =>
|
||||||
|
for {
|
||||||
|
now <- Timestamp.current[F]
|
||||||
|
_ <- ctx.logger.info(s" $now")
|
||||||
|
_ <- ctx.logger.info(s"Removed $ctx")
|
||||||
|
} yield ()
|
||||||
|
}
|
||||||
|
|
||||||
|
def onCancel[F[_]: Sync]: Task[F, NotifyDueItemsArgs, Unit] =
|
||||||
|
Task.log(_.warn("Cancelling notify-due-items task"))
|
||||||
|
|
||||||
|
}
|
@ -55,6 +55,6 @@ object Task {
|
|||||||
def setProgress[F[_]: Sync, A, B](n: Int)(data: B): Task[F, A, B] =
|
def setProgress[F[_]: Sync, A, B](n: Int)(data: B): Task[F, A, B] =
|
||||||
Task(_.setProgress(n).map(_ => data))
|
Task(_.setProgress(n).map(_ => data))
|
||||||
|
|
||||||
def log[F[_]](f: Logger[F] => F[Unit]): Task[F, Unit, Unit] =
|
def log[F[_], A](f: Logger[F] => F[Unit]): Task[F, A, Unit] =
|
||||||
Task(ctx => f(ctx.logger))
|
Task(ctx => f(ctx.logger))
|
||||||
}
|
}
|
||||||
|
@ -1560,10 +1560,10 @@ paths:
|
|||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: "#/components/schemas/BasicResult"
|
$ref: "#/components/schemas/BasicResult"
|
||||||
/sec/notification:
|
/sec/usertask/notifydueitems:
|
||||||
get:
|
get:
|
||||||
tags: [ Notification ]
|
tags: [ Notification ]
|
||||||
summary: Get current notification settings
|
summary: Get settings for "Notify Due Items" task
|
||||||
description: |
|
description: |
|
||||||
Return the current notification settings of the authenticated
|
Return the current notification settings of the authenticated
|
||||||
user. Users can be notified on due items via e-mail. This is
|
user. Users can be notified on due items via e-mail. This is
|
||||||
@ -1579,7 +1579,7 @@ paths:
|
|||||||
$ref: "#/components/schemas/NotificationData"
|
$ref: "#/components/schemas/NotificationData"
|
||||||
post:
|
post:
|
||||||
tags: [ Notification ]
|
tags: [ Notification ]
|
||||||
summary: Change current notification settings
|
summary: Change current settings for "Notify Due Items" task
|
||||||
description: |
|
description: |
|
||||||
Change the current notification settings of the authenticated
|
Change the current notification settings of the authenticated
|
||||||
user.
|
user.
|
||||||
@ -1607,6 +1607,7 @@ components:
|
|||||||
- id
|
- id
|
||||||
- enabled
|
- enabled
|
||||||
- smtpConnection
|
- smtpConnection
|
||||||
|
- recipients
|
||||||
- schedule
|
- schedule
|
||||||
- remindDays
|
- remindDays
|
||||||
- tagsInclude
|
- tagsInclude
|
||||||
@ -1620,6 +1621,11 @@ components:
|
|||||||
smtpConnection:
|
smtpConnection:
|
||||||
type: string
|
type: string
|
||||||
format: ident
|
format: ident
|
||||||
|
recipients:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
format: ident
|
||||||
schedule:
|
schedule:
|
||||||
type: string
|
type: string
|
||||||
format: calevent
|
format: calevent
|
||||||
|
@ -60,22 +60,23 @@ object RestServer {
|
|||||||
token: AuthToken
|
token: AuthToken
|
||||||
): HttpRoutes[F] =
|
): HttpRoutes[F] =
|
||||||
Router(
|
Router(
|
||||||
"auth" -> LoginRoutes.session(restApp.backend.login, cfg),
|
"auth" -> LoginRoutes.session(restApp.backend.login, cfg),
|
||||||
"tag" -> TagRoutes(restApp.backend, token),
|
"tag" -> TagRoutes(restApp.backend, token),
|
||||||
"equipment" -> EquipmentRoutes(restApp.backend, token),
|
"equipment" -> EquipmentRoutes(restApp.backend, token),
|
||||||
"organization" -> OrganizationRoutes(restApp.backend, token),
|
"organization" -> OrganizationRoutes(restApp.backend, token),
|
||||||
"person" -> PersonRoutes(restApp.backend, token),
|
"person" -> PersonRoutes(restApp.backend, token),
|
||||||
"source" -> SourceRoutes(restApp.backend, token),
|
"source" -> SourceRoutes(restApp.backend, token),
|
||||||
"user" -> UserRoutes(restApp.backend, token),
|
"user" -> UserRoutes(restApp.backend, token),
|
||||||
"collective" -> CollectiveRoutes(restApp.backend, token),
|
"collective" -> CollectiveRoutes(restApp.backend, token),
|
||||||
"queue" -> JobQueueRoutes(restApp.backend, token),
|
"queue" -> JobQueueRoutes(restApp.backend, token),
|
||||||
"item" -> ItemRoutes(restApp.backend, token),
|
"item" -> ItemRoutes(restApp.backend, token),
|
||||||
"attachment" -> AttachmentRoutes(restApp.backend, token),
|
"attachment" -> AttachmentRoutes(restApp.backend, token),
|
||||||
"upload" -> UploadRoutes.secured(restApp.backend, cfg, token),
|
"upload" -> UploadRoutes.secured(restApp.backend, cfg, token),
|
||||||
"checkfile" -> CheckFileRoutes.secured(restApp.backend, token),
|
"checkfile" -> CheckFileRoutes.secured(restApp.backend, token),
|
||||||
"email/send" -> MailSendRoutes(restApp.backend, token),
|
"email/send" -> MailSendRoutes(restApp.backend, token),
|
||||||
"email/settings" -> MailSettingsRoutes(restApp.backend, token),
|
"email/settings" -> MailSettingsRoutes(restApp.backend, token),
|
||||||
"email/sent" -> SentMailRoutes(restApp.backend, token)
|
"email/sent" -> SentMailRoutes(restApp.backend, token),
|
||||||
|
"usertask/notifydueitems" -> NotifyDueItemsRoutes(restApp.backend, token)
|
||||||
)
|
)
|
||||||
|
|
||||||
def openRoutes[F[_]: Effect](cfg: Config, restApp: RestApp[F]): HttpRoutes[F] =
|
def openRoutes[F[_]: Effect](cfg: Config, restApp: RestApp[F]): HttpRoutes[F] =
|
||||||
|
@ -472,6 +472,12 @@ trait Conversions {
|
|||||||
case PassChangeResult.UserNotFound => BasicResult(false, "User not found.")
|
case PassChangeResult.UserNotFound => BasicResult(false, "User not found.")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def basicResult(e: Either[Throwable, _], successMsg: String): BasicResult =
|
||||||
|
e match {
|
||||||
|
case Right(_) => BasicResult(true, successMsg)
|
||||||
|
case Left(ex) => BasicResult(false, ex.getMessage)
|
||||||
|
}
|
||||||
|
|
||||||
// MIME Type
|
// MIME Type
|
||||||
|
|
||||||
def fromContentType(header: `Content-Type`): MimeType =
|
def fromContentType(header: `Content-Type`): MimeType =
|
||||||
|
@ -0,0 +1,77 @@
|
|||||||
|
package docspell.restserver.routes
|
||||||
|
|
||||||
|
import cats.effect._
|
||||||
|
import cats.implicits._
|
||||||
|
import org.http4s._
|
||||||
|
import org.http4s.dsl.Http4sDsl
|
||||||
|
import org.http4s.circe.CirceEntityEncoder._
|
||||||
|
import org.http4s.circe.CirceEntityDecoder._
|
||||||
|
|
||||||
|
import docspell.backend.BackendApp
|
||||||
|
import docspell.backend.auth.AuthToken
|
||||||
|
import docspell.common._
|
||||||
|
import docspell.restapi.model._
|
||||||
|
import docspell.store.usertask._
|
||||||
|
import docspell.restserver.conv.Conversions
|
||||||
|
|
||||||
|
object NotifyDueItemsRoutes {
|
||||||
|
|
||||||
|
def apply[F[_]: Effect](backend: BackendApp[F], user: AuthToken): HttpRoutes[F] = {
|
||||||
|
val dsl = new Http4sDsl[F] {}
|
||||||
|
val ut = backend.userTask
|
||||||
|
import dsl._
|
||||||
|
|
||||||
|
HttpRoutes.of {
|
||||||
|
case GET -> Root =>
|
||||||
|
for {
|
||||||
|
task <- ut.getNotifyDueItems(user.account)
|
||||||
|
resp <- Ok(convert(task))
|
||||||
|
} yield resp
|
||||||
|
|
||||||
|
case req @ POST -> Root =>
|
||||||
|
for {
|
||||||
|
data <- req.as[NotificationSettings]
|
||||||
|
task = makeTask(user.account, data)
|
||||||
|
res <- ut
|
||||||
|
.submitNotifyDueItems(user.account, task)
|
||||||
|
.attempt
|
||||||
|
.map(Conversions.basicResult(_, "Update ok."))
|
||||||
|
resp <- Ok(res)
|
||||||
|
} yield resp
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def convert(task: UserTask[NotifyDueItemsArgs]): NotificationData =
|
||||||
|
NotificationData(taskToSettings(task), None, None)
|
||||||
|
|
||||||
|
def makeTask(
|
||||||
|
user: AccountId,
|
||||||
|
settings: NotificationSettings
|
||||||
|
): UserTask[NotifyDueItemsArgs] =
|
||||||
|
UserTask(
|
||||||
|
settings.id,
|
||||||
|
NotifyDueItemsArgs.taskName,
|
||||||
|
settings.enabled,
|
||||||
|
settings.schedule,
|
||||||
|
NotifyDueItemsArgs(
|
||||||
|
user,
|
||||||
|
settings.smtpConnection,
|
||||||
|
settings.recipients,
|
||||||
|
settings.remindDays,
|
||||||
|
settings.tagsInclude.map(Ident.unsafe),
|
||||||
|
settings.tagsExclude.map(Ident.unsafe)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def taskToSettings(task: UserTask[NotifyDueItemsArgs]): NotificationSettings =
|
||||||
|
NotificationSettings(
|
||||||
|
task.id,
|
||||||
|
task.enabled,
|
||||||
|
task.args.smtpConnection,
|
||||||
|
task.args.recipients,
|
||||||
|
task.timer,
|
||||||
|
task.args.remindDays,
|
||||||
|
task.args.tagsInclude.map(_.id),
|
||||||
|
task.args.tagsExclude.map(_.id)
|
||||||
|
)
|
||||||
|
}
|
@ -0,0 +1,89 @@
|
|||||||
|
package docspell.store.queries
|
||||||
|
|
||||||
|
import fs2._
|
||||||
|
import docspell.common._
|
||||||
|
import docspell.store.impl.Implicits._
|
||||||
|
import docspell.store.records._
|
||||||
|
import docspell.store.usertask.UserTask
|
||||||
|
import doobie._
|
||||||
|
|
||||||
|
object QUserTask {
|
||||||
|
private val cols = RPeriodicTask.Columns
|
||||||
|
|
||||||
|
def findAll(account: AccountId): Stream[ConnectionIO, UserTask[String]] =
|
||||||
|
selectSimple(
|
||||||
|
RPeriodicTask.Columns.all,
|
||||||
|
RPeriodicTask.table,
|
||||||
|
and(cols.group.is(account.collective), cols.submitter.is(account.user))
|
||||||
|
).query[RPeriodicTask].stream.map(makeUserTask)
|
||||||
|
|
||||||
|
def findByName(
|
||||||
|
account: AccountId,
|
||||||
|
name: Ident
|
||||||
|
): Stream[ConnectionIO, UserTask[String]] =
|
||||||
|
selectSimple(
|
||||||
|
RPeriodicTask.Columns.all,
|
||||||
|
RPeriodicTask.table,
|
||||||
|
and(
|
||||||
|
cols.group.is(account.collective),
|
||||||
|
cols.submitter.is(account.user),
|
||||||
|
cols.task.is(name)
|
||||||
|
)
|
||||||
|
).query[RPeriodicTask].stream.map(makeUserTask)
|
||||||
|
|
||||||
|
def insert(account: AccountId, task: UserTask[String]): ConnectionIO[Int] =
|
||||||
|
for {
|
||||||
|
r <- makePeriodicTask(account, task)
|
||||||
|
n <- RPeriodicTask.insert(r)
|
||||||
|
} yield n
|
||||||
|
|
||||||
|
def update(account: AccountId, task: UserTask[String]): ConnectionIO[Int] =
|
||||||
|
for {
|
||||||
|
r <- makePeriodicTask(account, task)
|
||||||
|
n <- RPeriodicTask.update(r)
|
||||||
|
} yield n
|
||||||
|
|
||||||
|
def exists(id: Ident): ConnectionIO[Boolean] =
|
||||||
|
RPeriodicTask.exists(id)
|
||||||
|
|
||||||
|
def delete(account: AccountId, id: Ident): ConnectionIO[Int] =
|
||||||
|
deleteFrom(
|
||||||
|
RPeriodicTask.table,
|
||||||
|
and(
|
||||||
|
cols.group.is(account.collective),
|
||||||
|
cols.submitter.is(account.user),
|
||||||
|
cols.id.is(id)
|
||||||
|
)
|
||||||
|
).update.run
|
||||||
|
|
||||||
|
def deleteAll(account: AccountId, name: Ident): ConnectionIO[Int] =
|
||||||
|
deleteFrom(
|
||||||
|
RPeriodicTask.table,
|
||||||
|
and(
|
||||||
|
cols.group.is(account.collective),
|
||||||
|
cols.submitter.is(account.user),
|
||||||
|
cols.task.is(name)
|
||||||
|
)
|
||||||
|
).update.run
|
||||||
|
|
||||||
|
def makeUserTask(r: RPeriodicTask): UserTask[String] =
|
||||||
|
UserTask(r.id, r.task, r.enabled, r.timer, r.args)
|
||||||
|
|
||||||
|
def makePeriodicTask(
|
||||||
|
account: AccountId,
|
||||||
|
t: UserTask[String]
|
||||||
|
): ConnectionIO[RPeriodicTask] =
|
||||||
|
RPeriodicTask
|
||||||
|
.create[ConnectionIO](
|
||||||
|
t.enabled,
|
||||||
|
t.name,
|
||||||
|
account.collective,
|
||||||
|
t.args,
|
||||||
|
s"${account.user.id}: ${t.name.id}",
|
||||||
|
account.user,
|
||||||
|
Priority.Low,
|
||||||
|
t.timer
|
||||||
|
)
|
||||||
|
.map(r => r.copy(id = t.id))
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,33 @@
|
|||||||
|
package docspell.store.usertask
|
||||||
|
|
||||||
|
import com.github.eikek.calev.CalEvent
|
||||||
|
import io.circe.Decoder
|
||||||
|
import io.circe.Encoder
|
||||||
|
import docspell.common._
|
||||||
|
import docspell.common.syntax.all._
|
||||||
|
|
||||||
|
case class UserTask[A](
|
||||||
|
id: Ident,
|
||||||
|
name: Ident,
|
||||||
|
enabled: Boolean,
|
||||||
|
timer: CalEvent,
|
||||||
|
args: A
|
||||||
|
) {
|
||||||
|
|
||||||
|
def encode(implicit E: Encoder[A]): UserTask[String] =
|
||||||
|
copy(args = E(args).noSpaces)
|
||||||
|
}
|
||||||
|
|
||||||
|
object UserTask {
|
||||||
|
|
||||||
|
|
||||||
|
implicit final class UserTaskCodec(ut: UserTask[String]) {
|
||||||
|
|
||||||
|
def decode[A](implicit D: Decoder[A]): Either[String, UserTask[A]] =
|
||||||
|
ut.args.parseJsonAs[A]
|
||||||
|
.left.map(_.getMessage)
|
||||||
|
.map(a => ut.copy(args = a))
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,173 @@
|
|||||||
|
package docspell.store.usertask
|
||||||
|
|
||||||
|
import fs2._
|
||||||
|
import cats.implicits._
|
||||||
|
import cats.effect._
|
||||||
|
import cats.data.OptionT
|
||||||
|
import _root_.io.circe._
|
||||||
|
import docspell.common._
|
||||||
|
import docspell.store.{AddResult, Store}
|
||||||
|
import docspell.store.queries.QUserTask
|
||||||
|
|
||||||
|
/** User tasks are `RPeriodicTask`s that can be managed by the user.
|
||||||
|
* The user can change arguments, enable/disable it or run it just
|
||||||
|
* once.
|
||||||
|
*
|
||||||
|
* This class defines methods at a higher level, dealing with
|
||||||
|
* `UserTask` and `AccountId` instead of directly using
|
||||||
|
* `RPeriodicTask`. A user task is associated to a specific user (not
|
||||||
|
* just the collective).
|
||||||
|
*
|
||||||
|
* @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
|
||||||
|
* id. A user may submit multiple same tasks (with different
|
||||||
|
* properties).
|
||||||
|
*/
|
||||||
|
trait UserTaskStore[F[_]] {
|
||||||
|
|
||||||
|
/** Return all tasks of the given user.
|
||||||
|
*/
|
||||||
|
def getAll(account: AccountId): 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]]
|
||||||
|
|
||||||
|
/** 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 D: Decoder[A]
|
||||||
|
): Stream[F, UserTask[A]]
|
||||||
|
|
||||||
|
/** Updates or inserts the given task.
|
||||||
|
*
|
||||||
|
* The task is identified by its id. If no task with this id
|
||||||
|
* exists, a new one is created. Otherwise the existing task is
|
||||||
|
* updated. The job executors are notified if a task has been
|
||||||
|
* enabled.
|
||||||
|
*/
|
||||||
|
def updateTask[A](account: AccountId, 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]
|
||||||
|
|
||||||
|
/** 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]]
|
||||||
|
|
||||||
|
/** 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 D: Decoder[A]
|
||||||
|
): OptionT[F, UserTask[A]]
|
||||||
|
|
||||||
|
/** Updates or inserts the given task.
|
||||||
|
*
|
||||||
|
* Unlike `updateTask`, this ensures that there is at most one task
|
||||||
|
* of some name in the db. Multiple same tasks (task with same
|
||||||
|
* name) may not be allowed to run, dependening on what they do.
|
||||||
|
* This is not ensured by the database, though.
|
||||||
|
*
|
||||||
|
* If there are currently mutliple tasks with same name as `ut` for
|
||||||
|
* the user `account`, they will all be removed and the given task
|
||||||
|
* inserted!
|
||||||
|
*/
|
||||||
|
def updateOneTask[A](account: AccountId, 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]
|
||||||
|
}
|
||||||
|
|
||||||
|
object UserTaskStore {
|
||||||
|
|
||||||
|
def apply[F[_]: Effect](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 getByNameRaw(account: AccountId, name: Ident): Stream[F, UserTask[String]] =
|
||||||
|
store.transact(QUserTask.findByName(account, name))
|
||||||
|
|
||||||
|
def getByName[A](account: AccountId, name: Ident)(
|
||||||
|
implicit D: Decoder[A]
|
||||||
|
): Stream[F, UserTask[A]] =
|
||||||
|
getByNameRaw(account, 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 E: Encoder[A]
|
||||||
|
): F[Int] = {
|
||||||
|
val exists = QUserTask.exists(ut.id)
|
||||||
|
val insert = QUserTask.insert(account, ut.encode)
|
||||||
|
store.add(insert, exists).flatMap {
|
||||||
|
case AddResult.Success =>
|
||||||
|
1.pure[F]
|
||||||
|
case AddResult.EntityExists(_) =>
|
||||||
|
store.transact(QUserTask.update(account, ut.encode))
|
||||||
|
case AddResult.Failure(ex) =>
|
||||||
|
Effect[F].raiseError(ex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def deleteTask(account: AccountId, id: Ident): F[Int] =
|
||||||
|
store.transact(QUserTask.delete(account, id))
|
||||||
|
|
||||||
|
def getOneByNameRaw(
|
||||||
|
account: AccountId,
|
||||||
|
name: Ident
|
||||||
|
): OptionT[F, UserTask[String]] =
|
||||||
|
OptionT(
|
||||||
|
getByNameRaw(account, name)
|
||||||
|
.take(2)
|
||||||
|
.compile
|
||||||
|
.toList
|
||||||
|
.flatMap {
|
||||||
|
case Nil => (None: Option[UserTask[String]]).pure[F]
|
||||||
|
case ut :: Nil => ut.some.pure[F]
|
||||||
|
case _ => Effect[F].raiseError(new Exception("More than one result found"))
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
def getOneByName[A](account: AccountId, name: Ident)(
|
||||||
|
implicit D: Decoder[A]
|
||||||
|
): OptionT[F, UserTask[A]] =
|
||||||
|
getOneByNameRaw(account, name)
|
||||||
|
.semiflatMap(_.decode match {
|
||||||
|
case Right(ua) => ua.pure[F]
|
||||||
|
case Left(err) => Effect[F].raiseError(new Exception(err))
|
||||||
|
})
|
||||||
|
|
||||||
|
def updateOneTask[A](account: AccountId, ut: UserTask[A])(
|
||||||
|
implicit E: Encoder[A]
|
||||||
|
): F[UserTask[String]] =
|
||||||
|
getByNameRaw(account, 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)))
|
||||||
|
} yield task
|
||||||
|
case Nil =>
|
||||||
|
val task = ut.encode
|
||||||
|
store.transact(QUserTask.insert(account, task)).map(_ => task)
|
||||||
|
}
|
||||||
|
|
||||||
|
def deleteAll(account: AccountId, name: Ident): F[Int] =
|
||||||
|
store.transact(QUserTask.deleteAll(account, name))
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
@ -88,6 +88,9 @@ object Dependencies {
|
|||||||
)
|
)
|
||||||
) ++ jclOverSlf4j
|
) ++ jclOverSlf4j
|
||||||
|
|
||||||
|
val emilCommon = Seq(
|
||||||
|
"com.github.eikek" %% "emil-common" % EmilVersion,
|
||||||
|
)
|
||||||
val emil = Seq(
|
val emil = Seq(
|
||||||
"com.github.eikek" %% "emil-common" % EmilVersion,
|
"com.github.eikek" %% "emil-common" % EmilVersion,
|
||||||
"com.github.eikek" %% "emil-javamail" % EmilVersion
|
"com.github.eikek" %% "emil-javamail" % EmilVersion
|
||||||
|
Loading…
x
Reference in New Issue
Block a user