mirror of
https://github.com/TheAnachronism/docspell.git
synced 2025-03-31 05:15:08 +00:00
Apply scalafmt to all files
This commit is contained in:
parent
57e274e2b0
commit
fc3e22e399
@ -27,7 +27,11 @@ trait BackendApp[F[_]] {
|
|||||||
|
|
||||||
object BackendApp {
|
object BackendApp {
|
||||||
|
|
||||||
def create[F[_]: ConcurrentEffect](cfg: Config, store: Store[F], httpClientEc: ExecutionContext): Resource[F, BackendApp[F]] =
|
def create[F[_]: ConcurrentEffect](
|
||||||
|
cfg: Config,
|
||||||
|
store: Store[F],
|
||||||
|
httpClientEc: ExecutionContext
|
||||||
|
): Resource[F, BackendApp[F]] =
|
||||||
for {
|
for {
|
||||||
queue <- JobQueue(store)
|
queue <- JobQueue(store)
|
||||||
loginImpl <- Login[F](store)
|
loginImpl <- Login[F](store)
|
||||||
@ -55,10 +59,12 @@ object BackendApp {
|
|||||||
val item = itemImpl
|
val item = itemImpl
|
||||||
}
|
}
|
||||||
|
|
||||||
def apply[F[_]: ConcurrentEffect: ContextShift](cfg: Config
|
def apply[F[_]: ConcurrentEffect: ContextShift](
|
||||||
, connectEC: ExecutionContext
|
cfg: Config,
|
||||||
, httpClientEc: ExecutionContext
|
connectEC: ExecutionContext,
|
||||||
, blocker: Blocker): Resource[F, BackendApp[F]] =
|
httpClientEc: ExecutionContext,
|
||||||
|
blocker: Blocker
|
||||||
|
): Resource[F, BackendApp[F]] =
|
||||||
for {
|
for {
|
||||||
store <- Store.create(cfg.jdbc, connectEC, blocker)
|
store <- Store.create(cfg.jdbc, connectEC, blocker)
|
||||||
backend <- create(cfg, store, httpClientEc)
|
backend <- create(cfg, store, httpClientEc)
|
||||||
|
@ -4,11 +4,7 @@ import docspell.backend.signup.{Config => SignupConfig}
|
|||||||
import docspell.common.MimeType
|
import docspell.common.MimeType
|
||||||
import docspell.store.JdbcConfig
|
import docspell.store.JdbcConfig
|
||||||
|
|
||||||
case class Config( jdbc: JdbcConfig
|
case class Config(jdbc: JdbcConfig, signup: SignupConfig, files: Config.Files) {}
|
||||||
, signup: SignupConfig
|
|
||||||
, files: Config.Files) {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
object Config {
|
object Config {
|
||||||
|
|
||||||
|
@ -50,14 +50,13 @@ object AuthToken {
|
|||||||
Left("Invalid authenticator")
|
Left("Invalid authenticator")
|
||||||
}
|
}
|
||||||
|
|
||||||
def user[F[_]: Sync](accountId: AccountId, key: ByteVector): F[AuthToken] = {
|
def user[F[_]: Sync](accountId: AccountId, key: ByteVector): F[AuthToken] =
|
||||||
for {
|
for {
|
||||||
salt <- Common.genSaltString[F]
|
salt <- Common.genSaltString[F]
|
||||||
millis = Instant.now.toEpochMilli
|
millis = Instant.now.toEpochMilli
|
||||||
cd = AuthToken(millis, accountId, salt, "")
|
cd = AuthToken(millis, accountId, salt, "")
|
||||||
sig = sign(cd, key)
|
sig = sign(cd, key)
|
||||||
} yield cd.copy(sig = sig)
|
} yield cd.copy(sig = sig)
|
||||||
}
|
|
||||||
|
|
||||||
private def sign(cd: AuthToken, key: ByteVector): String = {
|
private def sign(cd: AuthToken, key: ByteVector): String = {
|
||||||
val raw = cd.millis.toString + cd.account.asString + cd.salt
|
val raw = cd.millis.toString + cd.account.asString + cd.salt
|
||||||
|
@ -49,7 +49,8 @@ object Login {
|
|||||||
def invalidTime: Result = InvalidTime
|
def invalidTime: Result = InvalidTime
|
||||||
}
|
}
|
||||||
|
|
||||||
def apply[F[_]: Effect](store: Store[F]): Resource[F, Login[F]] = Resource.pure(new Login[F] {
|
def apply[F[_]: Effect](store: Store[F]): Resource[F, Login[F]] =
|
||||||
|
Resource.pure(new Login[F] {
|
||||||
|
|
||||||
def loginSession(config: Config)(sessionKey: String): F[Result] =
|
def loginSession(config: Config)(sessionKey: String): F[Result] =
|
||||||
AuthToken.fromString(sessionKey) match {
|
AuthToken.fromString(sessionKey) match {
|
||||||
@ -61,10 +62,10 @@ object Login {
|
|||||||
Result.invalidAuth.pure[F]
|
Result.invalidAuth.pure[F]
|
||||||
}
|
}
|
||||||
|
|
||||||
def loginUserPass(config: Config)(up: UserPass): F[Result] = {
|
def loginUserPass(config: Config)(up: UserPass): F[Result] =
|
||||||
AccountId.parse(up.user) match {
|
AccountId.parse(up.user) match {
|
||||||
case Right(acc) =>
|
case Right(acc) =>
|
||||||
val okResult=
|
val okResult =
|
||||||
store.transact(RUser.updateLogin(acc)) *>
|
store.transact(RUser.updateLogin(acc)) *>
|
||||||
AuthToken.user(acc, config.serverSecret).map(Result.ok)
|
AuthToken.user(acc, config.serverSecret).map(Result.ok)
|
||||||
for {
|
for {
|
||||||
@ -76,7 +77,6 @@ object Login {
|
|||||||
case Left(_) =>
|
case Left(_) =>
|
||||||
Result.invalidAuth.pure[F]
|
Result.invalidAuth.pure[F]
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private def check(given: String)(data: QLogin.Data): Boolean = {
|
private def check(given: String)(data: QLogin.Data): Boolean = {
|
||||||
val collOk = data.collectiveState == CollectiveState.Active ||
|
val collOk = data.collectiveState == CollectiveState.Active ||
|
||||||
|
@ -25,7 +25,11 @@ trait OCollective[F[_]] {
|
|||||||
|
|
||||||
def insights(collective: Ident): F[InsightData]
|
def insights(collective: Ident): F[InsightData]
|
||||||
|
|
||||||
def changePassword(accountId: AccountId, current: Password, newPass: Password): F[PassChangeResult]
|
def changePassword(
|
||||||
|
accountId: AccountId,
|
||||||
|
current: Password,
|
||||||
|
newPass: Password
|
||||||
|
): F[PassChangeResult]
|
||||||
}
|
}
|
||||||
|
|
||||||
object OCollective {
|
object OCollective {
|
||||||
@ -63,38 +67,46 @@ object OCollective {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def apply[F[_]: Effect](store: Store[F]): Resource[F, OCollective[F]] =
|
||||||
def apply[F[_]:Effect](store: Store[F]): Resource[F, OCollective[F]] =
|
|
||||||
Resource.pure(new OCollective[F] {
|
Resource.pure(new OCollective[F] {
|
||||||
def find(name: Ident): F[Option[RCollective]] =
|
def find(name: Ident): F[Option[RCollective]] =
|
||||||
store.transact(RCollective.findById(name))
|
store.transact(RCollective.findById(name))
|
||||||
|
|
||||||
def updateLanguage(collective: Ident, lang: Language): F[AddResult] =
|
def updateLanguage(collective: Ident, lang: Language): F[AddResult] =
|
||||||
store.transact(RCollective.updateLanguage(collective, lang)).
|
store
|
||||||
attempt.map(AddResult.fromUpdate)
|
.transact(RCollective.updateLanguage(collective, lang))
|
||||||
|
.attempt
|
||||||
|
.map(AddResult.fromUpdate)
|
||||||
|
|
||||||
def listUser(collective: Ident): F[Vector[RUser]] = {
|
def listUser(collective: Ident): F[Vector[RUser]] =
|
||||||
store.transact(RUser.findAll(collective, _.login))
|
store.transact(RUser.findAll(collective, _.login))
|
||||||
}
|
|
||||||
|
|
||||||
def add(s: RUser): F[AddResult] =
|
def add(s: RUser): F[AddResult] =
|
||||||
store.add(RUser.insert(s.copy(password = PasswordCrypt.crypt(s.password))), RUser.exists(s.login))
|
store.add(
|
||||||
|
RUser.insert(s.copy(password = PasswordCrypt.crypt(s.password))),
|
||||||
|
RUser.exists(s.login)
|
||||||
|
)
|
||||||
|
|
||||||
def update(s: RUser): F[AddResult] =
|
def update(s: RUser): F[AddResult] =
|
||||||
store.add(RUser.update(s), RUser.exists(s.login))
|
store.add(RUser.update(s), RUser.exists(s.login))
|
||||||
|
|
||||||
def deleteUser(login: Ident, collective: Ident): F[AddResult] =
|
def deleteUser(login: Ident, collective: Ident): F[AddResult] =
|
||||||
store.transact(RUser.delete(login, collective)).
|
store.transact(RUser.delete(login, collective)).attempt.map(AddResult.fromUpdate)
|
||||||
attempt.map(AddResult.fromUpdate)
|
|
||||||
|
|
||||||
def insights(collective: Ident): F[InsightData] =
|
def insights(collective: Ident): F[InsightData] =
|
||||||
store.transact(QCollective.getInsights(collective))
|
store.transact(QCollective.getInsights(collective))
|
||||||
|
|
||||||
def changePassword(accountId: AccountId, current: Password, newPass: Password): F[PassChangeResult] = {
|
def changePassword(
|
||||||
|
accountId: AccountId,
|
||||||
|
current: Password,
|
||||||
|
newPass: Password
|
||||||
|
): F[PassChangeResult] = {
|
||||||
val q = for {
|
val q = for {
|
||||||
optUser <- RUser.findByAccount(accountId)
|
optUser <- RUser.findByAccount(accountId)
|
||||||
check = optUser.map(_.password).map(p => PasswordCrypt.check(current, p))
|
check = optUser.map(_.password).map(p => PasswordCrypt.check(current, p))
|
||||||
n <- check.filter(identity).traverse(_ => RUser.updatePassword(accountId, PasswordCrypt.crypt(newPass)))
|
n <- check
|
||||||
|
.filter(identity)
|
||||||
|
.traverse(_ => RUser.updatePassword(accountId, PasswordCrypt.crypt(newPass)))
|
||||||
res = check match {
|
res = check match {
|
||||||
case Some(true) =>
|
case Some(true) =>
|
||||||
if (n.getOrElse(0) > 0) PassChangeResult.success else PassChangeResult.updateFailed
|
if (n.getOrElse(0) > 0) PassChangeResult.success else PassChangeResult.updateFailed
|
||||||
|
@ -17,7 +17,6 @@ trait OEquipment[F[_]] {
|
|||||||
def delete(id: Ident, collective: Ident): F[AddResult]
|
def delete(id: Ident, collective: Ident): F[AddResult]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
object OEquipment {
|
object OEquipment {
|
||||||
|
|
||||||
def apply[F[_]: Effect](store: Store[F]): Resource[F, OEquipment[F]] =
|
def apply[F[_]: Effect](store: Store[F]): Resource[F, OEquipment[F]] =
|
||||||
@ -46,9 +45,7 @@ object OEquipment {
|
|||||||
n0 <- RItem.removeConcEquip(collective, id)
|
n0 <- RItem.removeConcEquip(collective, id)
|
||||||
n1 <- REquipment.delete(id, collective)
|
n1 <- REquipment.delete(id, collective)
|
||||||
} yield n0 + n1
|
} yield n0 + n1
|
||||||
store.transact(io).
|
store.transact(io).attempt.map(AddResult.fromUpdate)
|
||||||
attempt.
|
|
||||||
map(AddResult.fromUpdate)
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -62,90 +62,98 @@ object OItem {
|
|||||||
|
|
||||||
case class AttachmentData[F[_]](ra: RAttachment, meta: FileMeta, data: Stream[F, Byte])
|
case class AttachmentData[F[_]](ra: RAttachment, meta: FileMeta, data: Stream[F, Byte])
|
||||||
|
|
||||||
|
|
||||||
def apply[F[_]: Effect](store: Store[F]): Resource[F, OItem[F]] =
|
def apply[F[_]: Effect](store: Store[F]): Resource[F, OItem[F]] =
|
||||||
Resource.pure(new OItem[F] {
|
Resource.pure(new OItem[F] {
|
||||||
|
|
||||||
def findItem(id: Ident, collective: Ident): F[Option[ItemData]] =
|
def findItem(id: Ident, collective: Ident): F[Option[ItemData]] =
|
||||||
store.transact(QItem.findItem(id)).
|
store.transact(QItem.findItem(id)).map(opt => opt.flatMap(_.filterCollective(collective)))
|
||||||
map(opt => opt.flatMap(_.filterCollective(collective)))
|
|
||||||
|
|
||||||
def findItems(q: Query, maxResults: Int): F[Vector[ListItem]] = {
|
def findItems(q: Query, maxResults: Int): F[Vector[ListItem]] =
|
||||||
store.transact(QItem.findItems(q).take(maxResults.toLong)).compile.toVector
|
store.transact(QItem.findItems(q).take(maxResults.toLong)).compile.toVector
|
||||||
}
|
|
||||||
|
|
||||||
def findAttachment(id: Ident, collective: Ident): F[Option[AttachmentData[F]]] = {
|
def findAttachment(id: Ident, collective: Ident): F[Option[AttachmentData[F]]] =
|
||||||
store.transact(RAttachment.findByIdAndCollective(id, collective)).
|
store
|
||||||
flatMap({
|
.transact(RAttachment.findByIdAndCollective(id, collective))
|
||||||
|
.flatMap({
|
||||||
case Some(ra) =>
|
case Some(ra) =>
|
||||||
store.bitpeace.get(ra.fileId.id).unNoneTerminate.compile.last.
|
store.bitpeace
|
||||||
map(_.map(m => AttachmentData[F](ra, m, store.bitpeace.fetchData2(RangeDef.all)(Stream.emit(m)))))
|
.get(ra.fileId.id)
|
||||||
|
.unNoneTerminate
|
||||||
|
.compile
|
||||||
|
.last
|
||||||
|
.map(
|
||||||
|
_.map(m =>
|
||||||
|
AttachmentData[F](
|
||||||
|
ra,
|
||||||
|
m,
|
||||||
|
store.bitpeace.fetchData2(RangeDef.all)(Stream.emit(m))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
case None =>
|
case None =>
|
||||||
(None: Option[AttachmentData[F]]).pure[F]
|
(None: Option[AttachmentData[F]]).pure[F]
|
||||||
})
|
})
|
||||||
}
|
|
||||||
|
|
||||||
def setTags(item: Ident, tagIds: List[Ident], collective: Ident): F[AddResult] = {
|
def setTags(item: Ident, tagIds: List[Ident], collective: Ident): F[AddResult] = {
|
||||||
val db = for {
|
val db = for {
|
||||||
cid <- RItem.getCollective(item)
|
cid <- RItem.getCollective(item)
|
||||||
nd <- if (cid.contains(collective)) RTagItem.deleteItemTags(item) else 0.pure[ConnectionIO]
|
nd <- if (cid.contains(collective)) RTagItem.deleteItemTags(item)
|
||||||
ni <- if (tagIds.nonEmpty && cid.contains(collective)) RTagItem.insertItemTags(item, tagIds) else 0.pure[ConnectionIO]
|
else 0.pure[ConnectionIO]
|
||||||
|
ni <- if (tagIds.nonEmpty && cid.contains(collective))
|
||||||
|
RTagItem.insertItemTags(item, tagIds)
|
||||||
|
else 0.pure[ConnectionIO]
|
||||||
} yield nd + ni
|
} yield nd + ni
|
||||||
|
|
||||||
store.transact(db).
|
store.transact(db).attempt.map(AddResult.fromUpdate)
|
||||||
attempt.
|
|
||||||
map(AddResult.fromUpdate)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
def setDirection(item: Ident, direction: Direction, collective: Ident): F[AddResult] =
|
def setDirection(item: Ident, direction: Direction, collective: Ident): F[AddResult] =
|
||||||
store.transact(RItem.updateDirection(item, collective, direction)).
|
store
|
||||||
attempt.
|
.transact(RItem.updateDirection(item, collective, direction))
|
||||||
map(AddResult.fromUpdate)
|
.attempt
|
||||||
|
.map(AddResult.fromUpdate)
|
||||||
|
|
||||||
def setCorrOrg(item: Ident, org: Option[Ident], collective: Ident): F[AddResult] =
|
def setCorrOrg(item: Ident, org: Option[Ident], collective: Ident): F[AddResult] =
|
||||||
store.transact(RItem.updateCorrOrg(item, collective, org)).
|
store.transact(RItem.updateCorrOrg(item, collective, org)).attempt.map(AddResult.fromUpdate)
|
||||||
attempt.
|
|
||||||
map(AddResult.fromUpdate)
|
|
||||||
|
|
||||||
def setCorrPerson(item: Ident, person: Option[Ident], collective: Ident): F[AddResult] =
|
def setCorrPerson(item: Ident, person: Option[Ident], collective: Ident): F[AddResult] =
|
||||||
store.transact(RItem.updateCorrPerson(item, collective, person)).
|
store
|
||||||
attempt.
|
.transact(RItem.updateCorrPerson(item, collective, person))
|
||||||
map(AddResult.fromUpdate)
|
.attempt
|
||||||
|
.map(AddResult.fromUpdate)
|
||||||
|
|
||||||
def setConcPerson(item: Ident, person: Option[Ident], collective: Ident): F[AddResult] =
|
def setConcPerson(item: Ident, person: Option[Ident], collective: Ident): F[AddResult] =
|
||||||
store.transact(RItem.updateConcPerson(item, collective, person)).
|
store
|
||||||
attempt.
|
.transact(RItem.updateConcPerson(item, collective, person))
|
||||||
map(AddResult.fromUpdate)
|
.attempt
|
||||||
|
.map(AddResult.fromUpdate)
|
||||||
|
|
||||||
def setConcEquip(item: Ident, equip: Option[Ident], collective: Ident): F[AddResult] =
|
def setConcEquip(item: Ident, equip: Option[Ident], collective: Ident): F[AddResult] =
|
||||||
store.transact(RItem.updateConcEquip(item, collective, equip)).
|
store
|
||||||
attempt.
|
.transact(RItem.updateConcEquip(item, collective, equip))
|
||||||
map(AddResult.fromUpdate)
|
.attempt
|
||||||
|
.map(AddResult.fromUpdate)
|
||||||
|
|
||||||
def setNotes(item: Ident, notes: Option[String], collective: Ident): F[AddResult] =
|
def setNotes(item: Ident, notes: Option[String], collective: Ident): F[AddResult] =
|
||||||
store.transact(RItem.updateNotes(item, collective, notes)).
|
store.transact(RItem.updateNotes(item, collective, notes)).attempt.map(AddResult.fromUpdate)
|
||||||
attempt.
|
|
||||||
map(AddResult.fromUpdate)
|
|
||||||
|
|
||||||
def setName(item: Ident, name: String, collective: Ident): F[AddResult] =
|
def setName(item: Ident, name: String, collective: Ident): F[AddResult] =
|
||||||
store.transact(RItem.updateName(item, collective, name)).
|
store.transact(RItem.updateName(item, collective, name)).attempt.map(AddResult.fromUpdate)
|
||||||
attempt.
|
|
||||||
map(AddResult.fromUpdate)
|
|
||||||
|
|
||||||
def setState(item: Ident, state: ItemState, collective: Ident): F[AddResult] =
|
def setState(item: Ident, state: ItemState, collective: Ident): F[AddResult] =
|
||||||
store.transact(RItem.updateStateForCollective(item, state, collective)).
|
store
|
||||||
attempt.
|
.transact(RItem.updateStateForCollective(item, state, collective))
|
||||||
map(AddResult.fromUpdate)
|
.attempt
|
||||||
|
.map(AddResult.fromUpdate)
|
||||||
|
|
||||||
def setItemDate(item: Ident, date: Option[Timestamp], collective: Ident): F[AddResult] =
|
def setItemDate(item: Ident, date: Option[Timestamp], collective: Ident): F[AddResult] =
|
||||||
store.transact(RItem.updateDate(item, collective, date)).
|
store.transact(RItem.updateDate(item, collective, date)).attempt.map(AddResult.fromUpdate)
|
||||||
attempt.
|
|
||||||
map(AddResult.fromUpdate)
|
|
||||||
|
|
||||||
def setItemDueDate(item: Ident, date: Option[Timestamp], collective: Ident): F[AddResult] =
|
def setItemDueDate(item: Ident, date: Option[Timestamp], collective: Ident): F[AddResult] =
|
||||||
store.transact(RItem.updateDueDate(item, collective, date)).
|
store
|
||||||
attempt.
|
.transact(RItem.updateDueDate(item, collective, date))
|
||||||
map(AddResult.fromUpdate)
|
.attempt
|
||||||
|
.map(AddResult.fromUpdate)
|
||||||
|
|
||||||
def delete(itemId: Ident, collective: Ident): F[Int] =
|
def delete(itemId: Ident, collective: Ident): F[Int] =
|
||||||
QItem.delete(store)(itemId, collective)
|
QItem.delete(store)(itemId, collective)
|
||||||
|
@ -36,15 +36,19 @@ object OJob {
|
|||||||
jobs.filter(_.job.state == JobState.Running)
|
jobs.filter(_.job.state == JobState.Running)
|
||||||
}
|
}
|
||||||
|
|
||||||
def apply[F[_]: ConcurrentEffect](store: Store[F], clientEC: ExecutionContext): Resource[F, OJob[F]] =
|
def apply[F[_]: ConcurrentEffect](
|
||||||
|
store: Store[F],
|
||||||
|
clientEC: ExecutionContext
|
||||||
|
): Resource[F, OJob[F]] =
|
||||||
Resource.pure(new OJob[F] {
|
Resource.pure(new OJob[F] {
|
||||||
|
|
||||||
def queueState(collective: Ident, maxResults: Int): F[CollectiveQueueState] = {
|
def queueState(collective: Ident, maxResults: Int): F[CollectiveQueueState] =
|
||||||
store.transact(QJob.queueStateSnapshot(collective).take(maxResults.toLong)).
|
store
|
||||||
map(t => JobDetail(t._1, t._2)).
|
.transact(QJob.queueStateSnapshot(collective).take(maxResults.toLong))
|
||||||
compile.toVector.
|
.map(t => JobDetail(t._1, t._2))
|
||||||
map(CollectiveQueueState)
|
.compile
|
||||||
}
|
.toVector
|
||||||
|
.map(CollectiveQueueState)
|
||||||
|
|
||||||
def cancelJob(id: Ident, collective: Ident): F[JobCancelResult] = {
|
def cancelJob(id: Ident, collective: Ident): F[JobCancelResult] = {
|
||||||
def mustCancel(job: Option[RJob]): Option[(RJob, Ident)] =
|
def mustCancel(job: Option[RJob]): Option[(RJob, Ident)] =
|
||||||
@ -66,8 +70,9 @@ object OJob {
|
|||||||
}
|
}
|
||||||
|
|
||||||
def tryCancel(job: RJob, worker: Ident): F[JobCancelResult] =
|
def tryCancel(job: RJob, worker: Ident): F[JobCancelResult] =
|
||||||
OJoex.cancelJob(job.id, worker, store, clientEC).
|
OJoex
|
||||||
map(flag => if (flag) JobCancelResult.CancelRequested else JobCancelResult.JobNotFound)
|
.cancelJob(job.id, worker, store, clientEC)
|
||||||
|
.map(flag => if (flag) JobCancelResult.CancelRequested else JobCancelResult.JobNotFound)
|
||||||
|
|
||||||
for {
|
for {
|
||||||
tryDel <- store.transact(tryDelete)
|
tryDel <- store.transact(tryDelete)
|
||||||
|
@ -13,24 +13,32 @@ import scala.concurrent.ExecutionContext
|
|||||||
import org.log4s._
|
import org.log4s._
|
||||||
|
|
||||||
object OJoex {
|
object OJoex {
|
||||||
private [this] val logger = getLogger
|
private[this] val logger = getLogger
|
||||||
|
|
||||||
def notifyAll[F[_]: ConcurrentEffect](store: Store[F], clientExecutionContext: ExecutionContext): F[Unit] = {
|
def notifyAll[F[_]: ConcurrentEffect](
|
||||||
|
store: Store[F],
|
||||||
|
clientExecutionContext: ExecutionContext
|
||||||
|
): F[Unit] =
|
||||||
for {
|
for {
|
||||||
nodes <- store.transact(RNode.findAll(NodeType.Joex))
|
nodes <- store.transact(RNode.findAll(NodeType.Joex))
|
||||||
_ <- nodes.toList.traverse(notifyJoex[F](clientExecutionContext))
|
_ <- nodes.toList.traverse(notifyJoex[F](clientExecutionContext))
|
||||||
} yield ()
|
} yield ()
|
||||||
}
|
|
||||||
|
|
||||||
def cancelJob[F[_]: ConcurrentEffect](jobId: Ident, worker: Ident, store: Store[F], clientEc: ExecutionContext): F[Boolean] =
|
def cancelJob[F[_]: ConcurrentEffect](
|
||||||
|
jobId: Ident,
|
||||||
|
worker: Ident,
|
||||||
|
store: Store[F],
|
||||||
|
clientEc: ExecutionContext
|
||||||
|
): F[Boolean] =
|
||||||
for {
|
for {
|
||||||
node <- store.transact(RNode.findById(worker))
|
node <- store.transact(RNode.findById(worker))
|
||||||
cancel <- node.traverse(joexCancel(clientEc)(_, jobId))
|
cancel <- node.traverse(joexCancel(clientEc)(_, jobId))
|
||||||
} yield cancel.getOrElse(false)
|
} yield cancel.getOrElse(false)
|
||||||
|
|
||||||
|
private def joexCancel[F[_]: ConcurrentEffect](
|
||||||
private def joexCancel[F[_]: ConcurrentEffect](ec: ExecutionContext)(node: RNode, job: Ident): F[Boolean] = {
|
ec: ExecutionContext
|
||||||
val notifyUrl = node.url/"api"/"v1"/"job"/job.id/"cancel"
|
)(node: RNode, job: Ident): F[Boolean] = {
|
||||||
|
val notifyUrl = node.url / "api" / "v1" / "job" / job.id / "cancel"
|
||||||
BlazeClientBuilder[F](ec).resource.use { client =>
|
BlazeClientBuilder[F](ec).resource.use { client =>
|
||||||
val req = Request[F](POST, Uri.unsafeFromString(notifyUrl.asString))
|
val req = Request[F](POST, Uri.unsafeFromString(notifyUrl.asString))
|
||||||
client.expect[String](req).map(_ => true)
|
client.expect[String](req).map(_ => true)
|
||||||
@ -38,7 +46,7 @@ object OJoex {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private def notifyJoex[F[_]: ConcurrentEffect](ec: ExecutionContext)(node: RNode): F[Unit] = {
|
private def notifyJoex[F[_]: ConcurrentEffect](ec: ExecutionContext)(node: RNode): F[Unit] = {
|
||||||
val notifyUrl = node.url/"api"/"v1"/"notify"
|
val notifyUrl = node.url / "api" / "v1" / "notify"
|
||||||
val execute = BlazeClientBuilder[F](ec).resource.use { client =>
|
val execute = BlazeClientBuilder[F](ec).resource.use { client =>
|
||||||
val req = Request[F](POST, Uri.unsafeFromString(notifyUrl.asString))
|
val req = Request[F](POST, Uri.unsafeFromString(notifyUrl.asString))
|
||||||
client.expect[String](req).map(_ => ())
|
client.expect[String](req).map(_ => ())
|
||||||
|
@ -36,13 +36,15 @@ object OOrganization {
|
|||||||
|
|
||||||
case class PersonAndContacts(person: RPerson, contacts: Seq[RContact])
|
case class PersonAndContacts(person: RPerson, contacts: Seq[RContact])
|
||||||
|
|
||||||
def apply[F[_] : Effect](store: Store[F]): Resource[F, OOrganization[F]] =
|
def apply[F[_]: Effect](store: Store[F]): Resource[F, OOrganization[F]] =
|
||||||
Resource.pure(new OOrganization[F] {
|
Resource.pure(new OOrganization[F] {
|
||||||
|
|
||||||
def findAllOrg(account: AccountId): F[Vector[OrgAndContacts]] =
|
def findAllOrg(account: AccountId): F[Vector[OrgAndContacts]] =
|
||||||
store.transact(QOrganization.findOrgAndContact(account.collective, _.name)).
|
store
|
||||||
map({ case (org, cont) => OrgAndContacts(org, cont) }).
|
.transact(QOrganization.findOrgAndContact(account.collective, _.name))
|
||||||
compile.toVector
|
.map({ case (org, cont) => OrgAndContacts(org, cont) })
|
||||||
|
.compile
|
||||||
|
.toVector
|
||||||
|
|
||||||
def findAllOrgRefs(account: AccountId): F[Vector[IdRef]] =
|
def findAllOrgRefs(account: AccountId): F[Vector[IdRef]] =
|
||||||
store.transact(ROrganization.findAllRef(account.collective, _.name))
|
store.transact(ROrganization.findAllRef(account.collective, _.name))
|
||||||
@ -54,9 +56,11 @@ object OOrganization {
|
|||||||
QOrganization.updateOrg(s.org, s.contacts, s.org.cid)(store)
|
QOrganization.updateOrg(s.org, s.contacts, s.org.cid)(store)
|
||||||
|
|
||||||
def findAllPerson(account: AccountId): F[Vector[PersonAndContacts]] =
|
def findAllPerson(account: AccountId): F[Vector[PersonAndContacts]] =
|
||||||
store.transact(QOrganization.findPersonAndContact(account.collective, _.name)).
|
store
|
||||||
map({ case (person, cont) => PersonAndContacts(person, cont) }).
|
.transact(QOrganization.findPersonAndContact(account.collective, _.name))
|
||||||
compile.toVector
|
.map({ case (person, cont) => PersonAndContacts(person, cont) })
|
||||||
|
.compile
|
||||||
|
.toVector
|
||||||
|
|
||||||
def findAllPersonRefs(account: AccountId): F[Vector[IdRef]] =
|
def findAllPersonRefs(account: AccountId): F[Vector[IdRef]] =
|
||||||
store.transact(RPerson.findAllRef(account.collective, _.name))
|
store.transact(RPerson.findAllRef(account.collective, _.name))
|
||||||
@ -68,14 +72,13 @@ object OOrganization {
|
|||||||
QOrganization.updatePerson(s.person, s.contacts, s.person.cid)(store)
|
QOrganization.updatePerson(s.person, s.contacts, s.person.cid)(store)
|
||||||
|
|
||||||
def deleteOrg(orgId: Ident, collective: Ident): F[AddResult] =
|
def deleteOrg(orgId: Ident, collective: Ident): F[AddResult] =
|
||||||
store.transact(QOrganization.deleteOrg(orgId, collective)).
|
store.transact(QOrganization.deleteOrg(orgId, collective)).attempt.map(AddResult.fromUpdate)
|
||||||
attempt.
|
|
||||||
map(AddResult.fromUpdate)
|
|
||||||
|
|
||||||
def deletePerson(personId: Ident, collective: Ident): F[AddResult] =
|
def deletePerson(personId: Ident, collective: Ident): F[AddResult] =
|
||||||
store.transact(QOrganization.deletePerson(personId, collective)).
|
store
|
||||||
attempt.
|
.transact(QOrganization.deletePerson(personId, collective))
|
||||||
map(AddResult.fromUpdate)
|
.attempt
|
||||||
|
.map(AddResult.fromUpdate)
|
||||||
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -41,8 +41,6 @@ object OSource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
def delete(id: Ident, collective: Ident): F[AddResult] =
|
def delete(id: Ident, collective: Ident): F[AddResult] =
|
||||||
store.transact(RSource.delete(id, collective)).
|
store.transact(RSource.delete(id, collective)).attempt.map(AddResult.fromUpdate)
|
||||||
attempt.
|
|
||||||
map(AddResult.fromUpdate)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -17,7 +17,6 @@ trait OTag[F[_]] {
|
|||||||
def delete(id: Ident, collective: Ident): F[AddResult]
|
def delete(id: Ident, collective: Ident): F[AddResult]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
object OTag {
|
object OTag {
|
||||||
|
|
||||||
def apply[F[_]: Effect](store: Store[F]): Resource[F, OTag[F]] =
|
def apply[F[_]: Effect](store: Store[F]): Resource[F, OTag[F]] =
|
||||||
@ -47,10 +46,7 @@ object OTag {
|
|||||||
n0 <- optTag.traverse(t => RTagItem.deleteTag(t.tagId))
|
n0 <- optTag.traverse(t => RTagItem.deleteTag(t.tagId))
|
||||||
n1 <- optTag.traverse(t => RTag.delete(t.tagId, collective))
|
n1 <- optTag.traverse(t => RTag.delete(t.tagId, collective))
|
||||||
} yield n0.getOrElse(0) + n1.getOrElse(0)
|
} yield n0.getOrElse(0) + n1.getOrElse(0)
|
||||||
store.transact(io).
|
store.transact(io).attempt.map(AddResult.fromUpdate)
|
||||||
attempt.
|
|
||||||
map(AddResult.fromUpdate)
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -22,17 +22,27 @@ trait OUpload[F[_]] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
object OUpload {
|
object OUpload {
|
||||||
private [this] val logger = getLogger
|
private[this] val logger = getLogger
|
||||||
|
|
||||||
case class File[F[_]](name: Option[String], advertisedMime: Option[MimeType], data: Stream[F, Byte])
|
case class File[F[_]](
|
||||||
|
name: Option[String],
|
||||||
|
advertisedMime: Option[MimeType],
|
||||||
|
data: Stream[F, Byte]
|
||||||
|
)
|
||||||
|
|
||||||
case class UploadMeta( direction: Option[Direction]
|
case class UploadMeta(
|
||||||
, sourceAbbrev: String
|
direction: Option[Direction],
|
||||||
, validFileTypes: Seq[MimeType])
|
sourceAbbrev: String,
|
||||||
|
validFileTypes: Seq[MimeType]
|
||||||
|
)
|
||||||
|
|
||||||
case class UploadData[F[_]]( multiple: Boolean
|
case class UploadData[F[_]](
|
||||||
, meta: UploadMeta
|
multiple: Boolean,
|
||||||
, files: Vector[File[F]], priority: Priority, tracker: Option[Ident])
|
meta: UploadMeta,
|
||||||
|
files: Vector[File[F]],
|
||||||
|
priority: Priority,
|
||||||
|
tracker: Option[Ident]
|
||||||
|
)
|
||||||
|
|
||||||
sealed trait UploadResult
|
sealed trait UploadResult
|
||||||
object UploadResult {
|
object UploadResult {
|
||||||
@ -41,22 +51,33 @@ object OUpload {
|
|||||||
case object NoSource extends UploadResult
|
case object NoSource extends UploadResult
|
||||||
}
|
}
|
||||||
|
|
||||||
def apply[F[_]: ConcurrentEffect](store: Store[F], queue: JobQueue[F], cfg: Config, httpClientEC: ExecutionContext): Resource[F, OUpload[F]] =
|
def apply[F[_]: ConcurrentEffect](
|
||||||
|
store: Store[F],
|
||||||
|
queue: JobQueue[F],
|
||||||
|
cfg: Config,
|
||||||
|
httpClientEC: ExecutionContext
|
||||||
|
): Resource[F, OUpload[F]] =
|
||||||
Resource.pure(new OUpload[F] {
|
Resource.pure(new OUpload[F] {
|
||||||
|
|
||||||
def submit(data: OUpload.UploadData[F], account: AccountId): F[OUpload.UploadResult] = {
|
def submit(data: OUpload.UploadData[F], account: AccountId): F[OUpload.UploadResult] =
|
||||||
for {
|
for {
|
||||||
files <- data.files.traverse(saveFile).map(_.flatten)
|
files <- data.files.traverse(saveFile).map(_.flatten)
|
||||||
pred <- checkFileList(files)
|
pred <- checkFileList(files)
|
||||||
lang <- store.transact(RCollective.findLanguage(account.collective))
|
lang <- store.transact(RCollective.findLanguage(account.collective))
|
||||||
meta = ProcessItemArgs.ProcessMeta(account.collective, lang.getOrElse(Language.German), data.meta.direction, data.meta.sourceAbbrev, data.meta.validFileTypes)
|
meta = ProcessItemArgs.ProcessMeta(
|
||||||
args = if (data.multiple) files.map(f => ProcessItemArgs(meta, List(f))) else Vector(ProcessItemArgs(meta, files.toList))
|
account.collective,
|
||||||
|
lang.getOrElse(Language.German),
|
||||||
|
data.meta.direction,
|
||||||
|
data.meta.sourceAbbrev,
|
||||||
|
data.meta.validFileTypes
|
||||||
|
)
|
||||||
|
args = if (data.multiple) files.map(f => ProcessItemArgs(meta, List(f)))
|
||||||
|
else Vector(ProcessItemArgs(meta, files.toList))
|
||||||
job <- pred.traverse(_ => makeJobs(args, account, data.priority, data.tracker))
|
job <- pred.traverse(_ => makeJobs(args, account, data.priority, data.tracker))
|
||||||
_ <- logger.fdebug(s"Storing jobs: $job")
|
_ <- logger.fdebug(s"Storing jobs: $job")
|
||||||
res <- job.traverse(submitJobs)
|
res <- job.traverse(submitJobs)
|
||||||
_ <- store.transact(RSource.incrementCounter(data.meta.sourceAbbrev, account.collective))
|
_ <- store.transact(RSource.incrementCounter(data.meta.sourceAbbrev, account.collective))
|
||||||
} yield res.fold(identity, identity)
|
} yield res.fold(identity, identity)
|
||||||
}
|
|
||||||
|
|
||||||
def submit(data: OUpload.UploadData[F], sourceId: Ident): F[OUpload.UploadResult] =
|
def submit(data: OUpload.UploadData[F], sourceId: Ident): F[OUpload.UploadResult] =
|
||||||
for {
|
for {
|
||||||
@ -67,30 +88,47 @@ object OUpload {
|
|||||||
result <- accId.traverse(acc => submit(updata, acc))
|
result <- accId.traverse(acc => submit(updata, acc))
|
||||||
} yield result.fold(identity, identity)
|
} yield result.fold(identity, identity)
|
||||||
|
|
||||||
private def submitJobs(jobs: Vector[RJob]): F[OUpload.UploadResult] = {
|
private def submitJobs(jobs: Vector[RJob]): F[OUpload.UploadResult] =
|
||||||
for {
|
for {
|
||||||
_ <- logger.fdebug(s"Storing jobs: $jobs")
|
_ <- logger.fdebug(s"Storing jobs: $jobs")
|
||||||
_ <- queue.insertAll(jobs)
|
_ <- queue.insertAll(jobs)
|
||||||
_ <- OJoex.notifyAll(store, httpClientEC)
|
_ <- OJoex.notifyAll(store, httpClientEC)
|
||||||
} yield UploadResult.Success
|
} yield UploadResult.Success
|
||||||
}
|
|
||||||
|
|
||||||
private def saveFile(file: File[F]): F[Option[ProcessItemArgs.File]] = {
|
private def saveFile(file: File[F]): F[Option[ProcessItemArgs.File]] =
|
||||||
logger.finfo(s"Receiving file $file") *>
|
logger.finfo(s"Receiving file $file") *>
|
||||||
store.bitpeace.saveNew(file.data, cfg.files.chunkSize, MimetypeHint(file.name, None), None).
|
store.bitpeace
|
||||||
compile.lastOrError.map(fm => Ident.unsafe(fm.id)).attempt.
|
.saveNew(file.data, cfg.files.chunkSize, MimetypeHint(file.name, None), None)
|
||||||
map(_.fold(ex => {
|
.compile
|
||||||
|
.lastOrError
|
||||||
|
.map(fm => Ident.unsafe(fm.id))
|
||||||
|
.attempt
|
||||||
|
.map(_.fold(ex => {
|
||||||
logger.warn(ex)(s"Could not store file for processing!")
|
logger.warn(ex)(s"Could not store file for processing!")
|
||||||
None
|
None
|
||||||
}, id => Some(ProcessItemArgs.File(file.name, id))))
|
}, id => Some(ProcessItemArgs.File(file.name, id))))
|
||||||
}
|
|
||||||
|
|
||||||
private def checkFileList(files: Seq[ProcessItemArgs.File]): F[Either[UploadResult, Unit]] =
|
private def checkFileList(files: Seq[ProcessItemArgs.File]): F[Either[UploadResult, Unit]] =
|
||||||
Effect[F].pure(if (files.isEmpty) Left(UploadResult.NoFiles) else Right(()))
|
Effect[F].pure(if (files.isEmpty) Left(UploadResult.NoFiles) else Right(()))
|
||||||
|
|
||||||
private def makeJobs(args: Vector[ProcessItemArgs], account: AccountId, prio: Priority, tracker: Option[Ident]): F[Vector[RJob]] = {
|
private def makeJobs(
|
||||||
|
args: Vector[ProcessItemArgs],
|
||||||
|
account: AccountId,
|
||||||
|
prio: Priority,
|
||||||
|
tracker: Option[Ident]
|
||||||
|
): F[Vector[RJob]] = {
|
||||||
def create(id: Ident, now: Timestamp, arg: ProcessItemArgs): RJob =
|
def create(id: Ident, now: Timestamp, arg: ProcessItemArgs): RJob =
|
||||||
RJob.newJob(id, ProcessItemArgs.taskName, account.collective, arg, arg.makeSubject, now, account.user, prio, tracker)
|
RJob.newJob(
|
||||||
|
id,
|
||||||
|
ProcessItemArgs.taskName,
|
||||||
|
account.collective,
|
||||||
|
arg,
|
||||||
|
arg.makeSubject,
|
||||||
|
now,
|
||||||
|
account.user,
|
||||||
|
prio,
|
||||||
|
tracker
|
||||||
|
)
|
||||||
|
|
||||||
for {
|
for {
|
||||||
id <- Ident.randomId[F]
|
id <- Ident.randomId[F]
|
||||||
|
@ -21,19 +21,19 @@ trait OSignup[F[_]] {
|
|||||||
object OSignup {
|
object OSignup {
|
||||||
private[this] val logger = getLogger
|
private[this] val logger = getLogger
|
||||||
|
|
||||||
def apply[F[_]:Effect](store: Store[F]): Resource[F, OSignup[F]] =
|
def apply[F[_]: Effect](store: Store[F]): Resource[F, OSignup[F]] =
|
||||||
Resource.pure(new OSignup[F] {
|
Resource.pure(new OSignup[F] {
|
||||||
|
|
||||||
def newInvite(cfg: Config)(password: Password): F[NewInviteResult] = {
|
def newInvite(cfg: Config)(password: Password): F[NewInviteResult] =
|
||||||
if (cfg.mode == Config.Mode.Invite) {
|
if (cfg.mode == Config.Mode.Invite) {
|
||||||
if (cfg.newInvitePassword.isEmpty || cfg.newInvitePassword != password) NewInviteResult.passwordMismatch.pure[F]
|
if (cfg.newInvitePassword.isEmpty || cfg.newInvitePassword != password)
|
||||||
|
NewInviteResult.passwordMismatch.pure[F]
|
||||||
else store.transact(RInvitation.insertNew).map(ri => NewInviteResult.success(ri.id))
|
else store.transact(RInvitation.insertNew).map(ri => NewInviteResult.success(ri.id))
|
||||||
} else {
|
} else {
|
||||||
Effect[F].pure(NewInviteResult.invitationClosed)
|
Effect[F].pure(NewInviteResult.invitationClosed)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
def register(cfg: Config)(data: RegisterData): F[SignupResult] = {
|
def register(cfg: Config)(data: RegisterData): F[SignupResult] =
|
||||||
cfg.mode match {
|
cfg.mode match {
|
||||||
case Config.Mode.Open =>
|
case Config.Mode.Open =>
|
||||||
addUser(data).map(SignupResult.fromAddResult)
|
addUser(data).map(SignupResult.fromAddResult)
|
||||||
@ -61,7 +61,6 @@ object OSignup {
|
|||||||
SignupResult.invalidInvitationKey.pure[F]
|
SignupResult.invalidInvitationKey.pure[F]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private def retryInvite(res: SignupResult): Boolean =
|
private def retryInvite(res: SignupResult): Boolean =
|
||||||
res match {
|
res match {
|
||||||
@ -77,22 +76,30 @@ object OSignup {
|
|||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private def addUser(data: RegisterData): F[AddResult] = {
|
private def addUser(data: RegisterData): F[AddResult] = {
|
||||||
def toRecords: F[(RCollective, RUser)] =
|
def toRecords: F[(RCollective, RUser)] =
|
||||||
for {
|
for {
|
||||||
id2 <- Ident.randomId[F]
|
id2 <- Ident.randomId[F]
|
||||||
now <- Timestamp.current[F]
|
now <- Timestamp.current[F]
|
||||||
c = RCollective(data.collName, CollectiveState.Active, Language.German, now)
|
c = RCollective(data.collName, CollectiveState.Active, Language.German, now)
|
||||||
u = RUser(id2, data.login, data.collName, PasswordCrypt.crypt(data.password), UserState.Active, None, 0, None, now)
|
u = RUser(
|
||||||
|
id2,
|
||||||
|
data.login,
|
||||||
|
data.collName,
|
||||||
|
PasswordCrypt.crypt(data.password),
|
||||||
|
UserState.Active,
|
||||||
|
None,
|
||||||
|
0,
|
||||||
|
None,
|
||||||
|
now
|
||||||
|
)
|
||||||
} yield (c, u)
|
} yield (c, u)
|
||||||
|
|
||||||
def insert(coll: RCollective, user: RUser): ConnectionIO[Int] = {
|
def insert(coll: RCollective, user: RUser): ConnectionIO[Int] =
|
||||||
for {
|
for {
|
||||||
n1 <- RCollective.insert(coll)
|
n1 <- RCollective.insert(coll)
|
||||||
n2 <- RUser.insert(user)
|
n2 <- RUser.insert(user)
|
||||||
} yield n1 + n2
|
} yield n1 + n2
|
||||||
}
|
|
||||||
|
|
||||||
def collectiveExists: ConnectionIO[Boolean] =
|
def collectiveExists: ConnectionIO[Boolean] =
|
||||||
RCollective.existsById(data.collName)
|
RCollective.existsById(data.collName)
|
||||||
|
@ -2,9 +2,7 @@ package docspell.backend.signup
|
|||||||
|
|
||||||
import docspell.store.AddResult
|
import docspell.store.AddResult
|
||||||
|
|
||||||
sealed trait SignupResult {
|
sealed trait SignupResult {}
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
object SignupResult {
|
object SignupResult {
|
||||||
|
|
||||||
|
@ -1,12 +1,14 @@
|
|||||||
package docspell.common
|
package docspell.common
|
||||||
|
|
||||||
case class Banner( component: String
|
case class Banner(
|
||||||
, version: String
|
component: String,
|
||||||
, gitHash: Option[String]
|
version: String,
|
||||||
, jdbcUrl: LenientUri
|
gitHash: Option[String],
|
||||||
, configFile: Option[String]
|
jdbcUrl: LenientUri,
|
||||||
, appId: Ident
|
configFile: Option[String],
|
||||||
, baseUrl: LenientUri) {
|
appId: Ident,
|
||||||
|
baseUrl: LenientUri
|
||||||
|
) {
|
||||||
|
|
||||||
private val banner =
|
private val banner =
|
||||||
"""______ _ _
|
"""______ _ _
|
||||||
@ -21,12 +23,12 @@ case class Banner( component: String
|
|||||||
|
|
||||||
def render(prefix: String): String = {
|
def render(prefix: String): String = {
|
||||||
val text = banner.split('\n').toList ++ List(
|
val text = banner.split('\n').toList ++ List(
|
||||||
s"<< $component >>"
|
s"<< $component >>",
|
||||||
, s"Id: ${appId.id}"
|
s"Id: ${appId.id}",
|
||||||
, s"Base-Url: ${baseUrl.asString}"
|
s"Base-Url: ${baseUrl.asString}",
|
||||||
, s"Database: ${jdbcUrl.asString}"
|
s"Database: ${jdbcUrl.asString}",
|
||||||
, s"Config: ${configFile.getOrElse("")}"
|
s"Config: ${configFile.getOrElse("")}",
|
||||||
, ""
|
""
|
||||||
)
|
)
|
||||||
|
|
||||||
text.map(line => s"$prefix $line").mkString("\n")
|
text.map(line => s"$prefix $line").mkString("\n")
|
||||||
|
@ -3,9 +3,7 @@ package docspell.common
|
|||||||
import io.circe._
|
import io.circe._
|
||||||
import io.circe.generic.semiauto._
|
import io.circe.generic.semiauto._
|
||||||
|
|
||||||
case class IdRef(id: Ident, name: String) {
|
case class IdRef(id: Ident, name: String) {}
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
object IdRef {
|
object IdRef {
|
||||||
|
|
||||||
|
@ -32,4 +32,3 @@ object ItemState {
|
|||||||
implicit val jsonEncoder: Encoder[ItemState] =
|
implicit val jsonEncoder: Encoder[ItemState] =
|
||||||
Encoder.encodeString.contramap(_.name)
|
Encoder.encodeString.contramap(_.name)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -31,14 +31,12 @@ object Language {
|
|||||||
|
|
||||||
def fromString(str: String): Either[String, Language] = {
|
def fromString(str: String): Either[String, Language] = {
|
||||||
val lang = str.toLowerCase
|
val lang = str.toLowerCase
|
||||||
all.find(_.allNames.contains(lang)).
|
all.find(_.allNames.contains(lang)).toRight(s"Unsupported or invalid language: $str")
|
||||||
toRight(s"Unsupported or invalid language: $str")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
def unsafe(str: String): Language =
|
def unsafe(str: String): Language =
|
||||||
fromString(str).fold(sys.error, identity)
|
fromString(str).fold(sys.error, identity)
|
||||||
|
|
||||||
|
|
||||||
implicit val jsonDecoder: Decoder[Language] =
|
implicit val jsonDecoder: Decoder[Language] =
|
||||||
Decoder.decodeString.emap(fromString)
|
Decoder.decodeString.emap(fromString)
|
||||||
implicit val jsonEncoder: Encoder[Language] =
|
implicit val jsonEncoder: Encoder[Language] =
|
||||||
|
@ -12,7 +12,7 @@ case class MimeType(primary: String, sub: String) {
|
|||||||
|
|
||||||
def matches(other: MimeType): Boolean =
|
def matches(other: MimeType): Boolean =
|
||||||
primary == other.primary &&
|
primary == other.primary &&
|
||||||
(sub == other.sub || sub == "*" )
|
(sub == other.sub || sub == "*")
|
||||||
}
|
}
|
||||||
|
|
||||||
object MimeType {
|
object MimeType {
|
||||||
@ -26,9 +26,10 @@ object MimeType {
|
|||||||
def image(sub: String): MimeType =
|
def image(sub: String): MimeType =
|
||||||
MimeType("image", partFromString(sub).throwLeft)
|
MimeType("image", partFromString(sub).throwLeft)
|
||||||
|
|
||||||
private[this] val validChars: Set[Char] = (('A' to 'Z') ++ ('a' to 'z') ++ ('0' to '9') ++ "*-").toSet
|
private[this] val validChars: Set[Char] =
|
||||||
|
(('A' to 'Z') ++ ('a' to 'z') ++ ('0' to '9') ++ "*-").toSet
|
||||||
|
|
||||||
def parse(str: String): Either[String, MimeType] = {
|
def parse(str: String): Either[String, MimeType] =
|
||||||
str.indexOf('/') match {
|
str.indexOf('/') match {
|
||||||
case -1 => Left(s"Invalid MIME type: $str")
|
case -1 => Left(s"Invalid MIME type: $str")
|
||||||
case n =>
|
case n =>
|
||||||
@ -37,7 +38,6 @@ object MimeType {
|
|||||||
sub <- partFromString(str.substring(n + 1))
|
sub <- partFromString(str.substring(n + 1))
|
||||||
} yield MimeType(prim.toLowerCase, sub.toLowerCase)
|
} yield MimeType(prim.toLowerCase, sub.toLowerCase)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
def unsafe(str: String): MimeType =
|
def unsafe(str: String): MimeType =
|
||||||
parse(str).throwLeft
|
parse(str).throwLeft
|
||||||
|
@ -2,6 +2,4 @@ package docspell.common
|
|||||||
|
|
||||||
import java.time.LocalDate
|
import java.time.LocalDate
|
||||||
|
|
||||||
case class NerDateLabel(date: LocalDate, label: NerLabel) {
|
case class NerDateLabel(date: LocalDate, label: NerLabel) {}
|
||||||
|
|
||||||
}
|
|
||||||
|
@ -3,9 +3,7 @@ package docspell.common
|
|||||||
import io.circe.generic.semiauto._
|
import io.circe.generic.semiauto._
|
||||||
import io.circe.{Decoder, Encoder}
|
import io.circe.{Decoder, Encoder}
|
||||||
|
|
||||||
case class NerLabel(label: String, tag: NerTag, startPosition: Int, endPosition: Int) {
|
case class NerLabel(label: String, tag: NerTag, startPosition: Int, endPosition: Int) {}
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
object NerLabel {
|
object NerLabel {
|
||||||
implicit val jsonEncoder: Encoder[NerLabel] = deriveEncoder[NerLabel]
|
implicit val jsonEncoder: Encoder[NerLabel] = deriveEncoder[NerLabel]
|
||||||
|
@ -4,7 +4,7 @@ import io.circe.{Decoder, Encoder}
|
|||||||
|
|
||||||
final class Password(val pass: String) extends AnyVal {
|
final class Password(val pass: String) extends AnyVal {
|
||||||
|
|
||||||
def isEmpty: Boolean= pass.isEmpty
|
def isEmpty: Boolean = pass.isEmpty
|
||||||
|
|
||||||
override def toString: String =
|
override def toString: String =
|
||||||
if (pass.isEmpty) "<empty>" else "***"
|
if (pass.isEmpty) "<empty>" else "***"
|
||||||
|
@ -16,7 +16,6 @@ object Priority {
|
|||||||
|
|
||||||
case object Low extends Priority
|
case object Low extends Priority
|
||||||
|
|
||||||
|
|
||||||
def fromString(str: String): Either[String, Priority] =
|
def fromString(str: String): Either[String, Priority] =
|
||||||
str.toLowerCase match {
|
str.toLowerCase match {
|
||||||
case "high" => Right(High)
|
case "high" => Right(High)
|
||||||
@ -27,7 +26,6 @@ object Priority {
|
|||||||
def unsafe(str: String): Priority =
|
def unsafe(str: String): Priority =
|
||||||
fromString(str).fold(sys.error, identity)
|
fromString(str).fold(sys.error, identity)
|
||||||
|
|
||||||
|
|
||||||
def fromInt(n: Int): Priority =
|
def fromInt(n: Int): Priority =
|
||||||
if (n <= toInt(Low)) Low
|
if (n <= toInt(Low)) Low
|
||||||
else High
|
else High
|
||||||
|
@ -6,14 +6,13 @@ import ProcessItemArgs._
|
|||||||
|
|
||||||
case class ProcessItemArgs(meta: ProcessMeta, files: List[File]) {
|
case class ProcessItemArgs(meta: ProcessMeta, files: List[File]) {
|
||||||
|
|
||||||
def makeSubject: String = {
|
def makeSubject: String =
|
||||||
files.flatMap(_.name) match {
|
files.flatMap(_.name) match {
|
||||||
case Nil => s"${meta.sourceAbbrev}: No files"
|
case Nil => s"${meta.sourceAbbrev}: No files"
|
||||||
case n :: Nil => n
|
case n :: Nil => n
|
||||||
case n1 :: n2 :: Nil => s"$n1, $n2"
|
case n1 :: n2 :: Nil => s"$n1, $n2"
|
||||||
case _ => s"${files.size} files from ${meta.sourceAbbrev}"
|
case _ => s"${files.size} files from ${meta.sourceAbbrev}"
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -21,11 +20,13 @@ object ProcessItemArgs {
|
|||||||
|
|
||||||
val taskName = Ident.unsafe("process-item")
|
val taskName = Ident.unsafe("process-item")
|
||||||
|
|
||||||
case class ProcessMeta( collective: Ident
|
case class ProcessMeta(
|
||||||
, language: Language
|
collective: Ident,
|
||||||
, direction: Option[Direction]
|
language: Language,
|
||||||
, sourceAbbrev: String
|
direction: Option[Direction],
|
||||||
, validFileTypes: Seq[MimeType])
|
sourceAbbrev: String,
|
||||||
|
validFileTypes: Seq[MimeType]
|
||||||
|
)
|
||||||
|
|
||||||
object ProcessMeta {
|
object ProcessMeta {
|
||||||
implicit val jsonEncoder: Encoder[ProcessMeta] = deriveEncoder[ProcessMeta]
|
implicit val jsonEncoder: Encoder[ProcessMeta] = deriveEncoder[ProcessMeta]
|
||||||
|
@ -30,8 +30,6 @@ object Timestamp {
|
|||||||
def current[F[_]: Sync]: F[Timestamp] =
|
def current[F[_]: Sync]: F[Timestamp] =
|
||||||
Sync[F].delay(Timestamp(Instant.now))
|
Sync[F].delay(Timestamp(Instant.now))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
implicit val encodeTimestamp: Encoder[Timestamp] =
|
implicit val encodeTimestamp: Encoder[Timestamp] =
|
||||||
BaseJsonCodecs.encodeInstantEpoch.contramap(_.value)
|
BaseJsonCodecs.encodeInstantEpoch.contramap(_.value)
|
||||||
|
|
||||||
|
@ -12,7 +12,6 @@ object UserState {
|
|||||||
/** The user is blocked by an admin. */
|
/** The user is blocked by an admin. */
|
||||||
case object Disabled extends UserState
|
case object Disabled extends UserState
|
||||||
|
|
||||||
|
|
||||||
def fromString(s: String): Either[String, UserState] =
|
def fromString(s: String): Either[String, UserState] =
|
||||||
s.toLowerCase match {
|
s.toLowerCase match {
|
||||||
case "active" => Right(Active)
|
case "active" => Right(Active)
|
||||||
|
@ -11,13 +11,18 @@ trait StreamSyntax {
|
|||||||
implicit class StringStreamOps[F[_]](s: Stream[F, String]) {
|
implicit class StringStreamOps[F[_]](s: Stream[F, String]) {
|
||||||
|
|
||||||
def parseJsonAs[A](implicit d: Decoder[A], F: Sync[F]): F[Either[Throwable, A]] =
|
def parseJsonAs[A](implicit d: Decoder[A], F: Sync[F]): F[Either[Throwable, A]] =
|
||||||
s.fold("")(_ + _).
|
s.fold("")(_ + _)
|
||||||
compile.last.
|
.compile
|
||||||
map(optStr => for {
|
.last
|
||||||
str <- optStr.map(_.trim).toRight(new Exception("Empty string cannot be parsed into a value"))
|
.map(optStr =>
|
||||||
|
for {
|
||||||
|
str <- optStr
|
||||||
|
.map(_.trim)
|
||||||
|
.toRight(new Exception("Empty string cannot be parsed into a value"))
|
||||||
json <- parse(str).leftMap(_.underlying)
|
json <- parse(str).leftMap(_.underlying)
|
||||||
value <- json.as[A]
|
value <- json.as[A]
|
||||||
} yield value)
|
} yield value
|
||||||
|
)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -5,17 +5,23 @@ import docspell.joex.scheduler.SchedulerConfig
|
|||||||
import docspell.store.JdbcConfig
|
import docspell.store.JdbcConfig
|
||||||
import docspell.text.ocr.{Config => OcrConfig}
|
import docspell.text.ocr.{Config => OcrConfig}
|
||||||
|
|
||||||
case class Config(appId: Ident
|
case class Config(
|
||||||
, baseUrl: LenientUri
|
appId: Ident,
|
||||||
, bind: Config.Bind
|
baseUrl: LenientUri,
|
||||||
, jdbc: JdbcConfig
|
bind: Config.Bind,
|
||||||
, scheduler: SchedulerConfig
|
jdbc: JdbcConfig,
|
||||||
, extraction: OcrConfig
|
scheduler: SchedulerConfig,
|
||||||
|
extraction: OcrConfig
|
||||||
)
|
)
|
||||||
|
|
||||||
object Config {
|
object Config {
|
||||||
val postgres = JdbcConfig(LenientUri.unsafe("jdbc:postgresql://localhost:5432/docspelldev"), "dev", "dev")
|
val postgres =
|
||||||
val h2 = JdbcConfig(LenientUri.unsafe("jdbc:h2:./target/docspelldev.db;MODE=PostgreSQL;DATABASE_TO_LOWER=TRUE"), "sa", "")
|
JdbcConfig(LenientUri.unsafe("jdbc:postgresql://localhost:5432/docspelldev"), "dev", "dev")
|
||||||
|
val h2 = JdbcConfig(
|
||||||
|
LenientUri.unsafe("jdbc:h2:./target/docspelldev.db;MODE=PostgreSQL;DATABASE_TO_LOWER=TRUE"),
|
||||||
|
"sa",
|
||||||
|
""
|
||||||
|
)
|
||||||
|
|
||||||
case class Bind(address: String, port: Int)
|
case class Bind(address: String, port: Int)
|
||||||
}
|
}
|
||||||
|
@ -11,7 +11,6 @@ object ConfigFile {
|
|||||||
def loadConfig: Config =
|
def loadConfig: Config =
|
||||||
ConfigSource.default.at("docspell.joex").loadOrThrow[Config]
|
ConfigSource.default.at("docspell.joex").loadOrThrow[Config]
|
||||||
|
|
||||||
|
|
||||||
object Implicits {
|
object Implicits {
|
||||||
implicit val countingSchemeReader: ConfigReader[CountingScheme] =
|
implicit val countingSchemeReader: ConfigReader[CountingScheme] =
|
||||||
ConfigReader[String].emap(reason(CountingScheme.readString))
|
ConfigReader[String].emap(reason(CountingScheme.readString))
|
||||||
|
@ -12,11 +12,13 @@ import fs2.concurrent.SignallingRef
|
|||||||
|
|
||||||
import scala.concurrent.ExecutionContext
|
import scala.concurrent.ExecutionContext
|
||||||
|
|
||||||
final class JoexAppImpl[F[_]: ConcurrentEffect : ContextShift: Timer]( cfg: Config
|
final class JoexAppImpl[F[_]: ConcurrentEffect: ContextShift: Timer](
|
||||||
, nodeOps: ONode[F]
|
cfg: Config,
|
||||||
, store: Store[F]
|
nodeOps: ONode[F],
|
||||||
, termSignal: SignallingRef[F, Boolean]
|
store: Store[F],
|
||||||
, val scheduler: Scheduler[F]) extends JoexApp[F] {
|
termSignal: SignallingRef[F, Boolean],
|
||||||
|
val scheduler: Scheduler[F]
|
||||||
|
) extends JoexApp[F] {
|
||||||
|
|
||||||
def init: F[Unit] = {
|
def init: F[Unit] = {
|
||||||
val run = scheduler.start.compile.drain
|
val run = scheduler.start.compile.drain
|
||||||
@ -40,16 +42,24 @@ final class JoexAppImpl[F[_]: ConcurrentEffect : ContextShift: Timer]( cfg: Conf
|
|||||||
|
|
||||||
object JoexAppImpl {
|
object JoexAppImpl {
|
||||||
|
|
||||||
def create[F[_]: ConcurrentEffect : ContextShift: Timer](cfg: Config
|
def create[F[_]: ConcurrentEffect: ContextShift: Timer](
|
||||||
, termSignal: SignallingRef[F, Boolean]
|
cfg: Config,
|
||||||
, connectEC: ExecutionContext
|
termSignal: SignallingRef[F, Boolean],
|
||||||
, blocker: Blocker): Resource[F, JoexApp[F]] =
|
connectEC: ExecutionContext,
|
||||||
|
blocker: Blocker
|
||||||
|
): Resource[F, JoexApp[F]] =
|
||||||
for {
|
for {
|
||||||
store <- Store.create(cfg.jdbc, connectEC, blocker)
|
store <- Store.create(cfg.jdbc, connectEC, blocker)
|
||||||
nodeOps <- ONode(store)
|
nodeOps <- ONode(store)
|
||||||
sch <- SchedulerBuilder(cfg.scheduler, blocker, store).
|
sch <- SchedulerBuilder(cfg.scheduler, blocker, store)
|
||||||
withTask(JobTask.json(ProcessItemArgs.taskName, ItemHandler[F](cfg.extraction), ItemHandler.onCancel[F])).
|
.withTask(
|
||||||
resource
|
JobTask.json(
|
||||||
|
ProcessItemArgs.taskName,
|
||||||
|
ItemHandler[F](cfg.extraction),
|
||||||
|
ItemHandler.onCancel[F]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.resource
|
||||||
app = new JoexAppImpl(cfg, nodeOps, store, termSignal, sch)
|
app = new JoexAppImpl(cfg, nodeOps, store, termSignal, sch)
|
||||||
appR <- Resource.make(app.init.map(_ => app))(_.shutdown)
|
appR <- Resource.make(app.init.map(_ => app))(_.shutdown)
|
||||||
} yield appR
|
} yield appR
|
||||||
|
@ -15,11 +15,17 @@ import scala.concurrent.ExecutionContext
|
|||||||
|
|
||||||
object JoexServer {
|
object JoexServer {
|
||||||
|
|
||||||
|
private case class App[F[_]](
|
||||||
|
httpApp: HttpApp[F],
|
||||||
|
termSig: SignallingRef[F, Boolean],
|
||||||
|
exitRef: Ref[F, ExitCode]
|
||||||
|
)
|
||||||
|
|
||||||
private case class App[F[_]](httpApp: HttpApp[F], termSig: SignallingRef[F, Boolean], exitRef: Ref[F, ExitCode])
|
def stream[F[_]: ConcurrentEffect: ContextShift](
|
||||||
|
cfg: Config,
|
||||||
def stream[F[_]: ConcurrentEffect : ContextShift](cfg: Config, connectEC: ExecutionContext, blocker: Blocker)
|
connectEC: ExecutionContext,
|
||||||
(implicit T: Timer[F]): Stream[F, Nothing] = {
|
blocker: Blocker
|
||||||
|
)(implicit T: Timer[F]): Stream[F, Nothing] = {
|
||||||
|
|
||||||
val app = for {
|
val app = for {
|
||||||
signal <- Resource.liftF(SignallingRef[F, Boolean](false))
|
signal <- Resource.liftF(SignallingRef[F, Boolean](false))
|
||||||
@ -36,13 +42,14 @@ object JoexServer {
|
|||||||
|
|
||||||
} yield App(finalHttpApp, signal, exitCode)
|
} yield App(finalHttpApp, signal, exitCode)
|
||||||
|
|
||||||
|
Stream
|
||||||
Stream.resource(app).flatMap(app =>
|
.resource(app)
|
||||||
BlazeServerBuilder[F].
|
.flatMap(app =>
|
||||||
bindHttp(cfg.bind.port, cfg.bind.address).
|
BlazeServerBuilder[F]
|
||||||
withHttpApp(app.httpApp).
|
.bindHttp(cfg.bind.port, cfg.bind.address)
|
||||||
withoutBanner.
|
.withHttpApp(app.httpApp)
|
||||||
serveWhile(app.termSig, app.exitRef)
|
.withoutBanner
|
||||||
|
.serveWhile(app.termSig, app.exitRef)
|
||||||
)
|
)
|
||||||
|
|
||||||
}.drain
|
}.drain
|
||||||
|
@ -14,10 +14,12 @@ object Main extends IOApp {
|
|||||||
private[this] val logger = getLogger
|
private[this] val logger = getLogger
|
||||||
|
|
||||||
val blockingEc: ExecutionContext = ExecutionContext.fromExecutor(
|
val blockingEc: ExecutionContext = ExecutionContext.fromExecutor(
|
||||||
Executors.newCachedThreadPool(ThreadFactories.ofName("docspell-joex-blocking")))
|
Executors.newCachedThreadPool(ThreadFactories.ofName("docspell-joex-blocking"))
|
||||||
|
)
|
||||||
val blocker = Blocker.liftExecutionContext(blockingEc)
|
val blocker = Blocker.liftExecutionContext(blockingEc)
|
||||||
val connectEC: ExecutionContext = ExecutionContext.fromExecutorService(
|
val connectEC: ExecutionContext = ExecutionContext.fromExecutorService(
|
||||||
Executors.newFixedThreadPool(5, ThreadFactories.ofName("docspell-joex-dbconnect")))
|
Executors.newFixedThreadPool(5, ThreadFactories.ofName("docspell-joex-dbconnect"))
|
||||||
|
)
|
||||||
|
|
||||||
def run(args: List[String]) = {
|
def run(args: List[String]) = {
|
||||||
args match {
|
args match {
|
||||||
@ -40,12 +42,15 @@ object Main extends IOApp {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val cfg = ConfigFile.loadConfig
|
val cfg = ConfigFile.loadConfig
|
||||||
val banner = Banner("JOEX"
|
val banner = Banner(
|
||||||
, BuildInfo.version
|
"JOEX",
|
||||||
, BuildInfo.gitHeadCommit
|
BuildInfo.version,
|
||||||
, cfg.jdbc.url
|
BuildInfo.gitHeadCommit,
|
||||||
, Option(System.getProperty("config.file"))
|
cfg.jdbc.url,
|
||||||
, cfg.appId, cfg.baseUrl)
|
Option(System.getProperty("config.file")),
|
||||||
|
cfg.appId,
|
||||||
|
cfg.baseUrl
|
||||||
|
)
|
||||||
logger.info(s"\n${banner.render("***>")}")
|
logger.info(s"\n${banner.render("***>")}")
|
||||||
JoexServer.stream[IO](cfg, connectEC, blocker).compile.drain.as(ExitCode.Success)
|
JoexServer.stream[IO](cfg, connectEC, blocker).compile.drain.as(ExitCode.Success)
|
||||||
}
|
}
|
||||||
|
@ -23,20 +23,28 @@ object CreateItem {
|
|||||||
Task { ctx =>
|
Task { ctx =>
|
||||||
val validFiles = ctx.args.meta.validFileTypes.map(_.asString).toSet
|
val validFiles = ctx.args.meta.validFileTypes.map(_.asString).toSet
|
||||||
|
|
||||||
def fileMetas(itemId: Ident, now: Timestamp) = Stream.emits(ctx.args.files).
|
def fileMetas(itemId: Ident, now: Timestamp) =
|
||||||
flatMap(f => ctx.store.bitpeace.get(f.fileMetaId.id).map(fm => (f, fm))).
|
Stream
|
||||||
collect({ case (f, Some(fm)) if validFiles.contains(fm.mimetype.baseType) => f }).
|
.emits(ctx.args.files)
|
||||||
zipWithIndex.
|
.flatMap(f => ctx.store.bitpeace.get(f.fileMetaId.id).map(fm => (f, fm)))
|
||||||
evalMap({ case (f, index) =>
|
.collect({ case (f, Some(fm)) if validFiles.contains(fm.mimetype.baseType) => f })
|
||||||
Ident.randomId[F].map(id => RAttachment(id, itemId, f.fileMetaId, index.toInt, now, f.name))
|
.zipWithIndex
|
||||||
}).
|
.evalMap({
|
||||||
compile.toVector
|
case (f, index) =>
|
||||||
|
Ident
|
||||||
|
.randomId[F]
|
||||||
|
.map(id => RAttachment(id, itemId, f.fileMetaId, index.toInt, now, f.name))
|
||||||
|
})
|
||||||
|
.compile
|
||||||
|
.toVector
|
||||||
|
|
||||||
val item = RItem.newItem[F](ctx.args.meta.collective
|
val item = RItem.newItem[F](
|
||||||
, ctx.args.makeSubject
|
ctx.args.meta.collective,
|
||||||
, ctx.args.meta.sourceAbbrev
|
ctx.args.makeSubject,
|
||||||
, ctx.args.meta.direction.getOrElse(Direction.Incoming)
|
ctx.args.meta.sourceAbbrev,
|
||||||
, ItemState.Premature)
|
ctx.args.meta.direction.getOrElse(Direction.Incoming),
|
||||||
|
ItemState.Premature
|
||||||
|
)
|
||||||
|
|
||||||
for {
|
for {
|
||||||
_ <- ctx.logger.info(s"Creating new item with ${ctx.args.files.size} attachment(s)")
|
_ <- ctx.logger.info(s"Creating new item with ${ctx.args.files.size} attachment(s)")
|
||||||
@ -56,16 +64,28 @@ object CreateItem {
|
|||||||
Task { ctx =>
|
Task { ctx =>
|
||||||
for {
|
for {
|
||||||
cand <- ctx.store.transact(QItem.findByFileIds(ctx.args.files.map(_.fileMetaId)))
|
cand <- ctx.store.transact(QItem.findByFileIds(ctx.args.files.map(_.fileMetaId)))
|
||||||
_ <- if (cand.nonEmpty) ctx.logger.warn("Found existing item with these files.") else ().pure[F]
|
_ <- if (cand.nonEmpty) ctx.logger.warn("Found existing item with these files.")
|
||||||
|
else ().pure[F]
|
||||||
ht <- cand.drop(1).traverse(ri => QItem.delete(ctx.store)(ri.id, ri.cid))
|
ht <- cand.drop(1).traverse(ri => QItem.delete(ctx.store)(ri.id, ri.cid))
|
||||||
_ <- if (ht.sum > 0) ctx.logger.warn(s"Removed ${ht.sum} items with same attachments") else ().pure[F]
|
_ <- if (ht.sum > 0) ctx.logger.warn(s"Removed ${ht.sum} items with same attachments")
|
||||||
rms <- cand.headOption.traverse(ri => ctx.store.transact(RAttachment.findByItemAndCollective(ri.id, ri.cid)))
|
else ().pure[F]
|
||||||
} yield cand.headOption.map(ri => ItemData(ri, rms.getOrElse(Vector.empty), Vector.empty, Vector.empty))
|
rms <- cand.headOption.traverse(ri =>
|
||||||
|
ctx.store.transact(RAttachment.findByItemAndCollective(ri.id, ri.cid))
|
||||||
|
)
|
||||||
|
} yield cand.headOption.map(ri =>
|
||||||
|
ItemData(ri, rms.getOrElse(Vector.empty), Vector.empty, Vector.empty)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private def logDifferences[F[_]: Sync](ctx: Context[F, ProcessItemArgs], saved: Vector[RAttachment], saveCount: Int): F[Unit] =
|
private def logDifferences[F[_]: Sync](
|
||||||
|
ctx: Context[F, ProcessItemArgs],
|
||||||
|
saved: Vector[RAttachment],
|
||||||
|
saveCount: Int
|
||||||
|
): F[Unit] =
|
||||||
if (ctx.args.files.size != saved.size) {
|
if (ctx.args.files.size != saved.size) {
|
||||||
ctx.logger.warn(s"Not all given files (${ctx.args.files.size}) have been stored. Files retained: ${saved.size}; saveCount=$saveCount")
|
ctx.logger.warn(
|
||||||
|
s"Not all given files (${ctx.args.files.size}) have been stored. Files retained: ${saved.size}; saveCount=$saveCount"
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
().pure[F]
|
().pure[F]
|
||||||
}
|
}
|
||||||
|
@ -19,45 +19,65 @@ object FindProposal {
|
|||||||
|
|
||||||
def apply[F[_]: Sync](data: ItemData): Task[F, ProcessItemArgs, ItemData] =
|
def apply[F[_]: Sync](data: ItemData): Task[F, ProcessItemArgs, ItemData] =
|
||||||
Task { ctx =>
|
Task { ctx =>
|
||||||
val rmas = data.metas.map(rm =>
|
val rmas = data.metas.map(rm => rm.copy(nerlabels = removeDuplicates(rm.nerlabels)))
|
||||||
rm.copy(nerlabels = removeDuplicates(rm.nerlabels)))
|
|
||||||
|
|
||||||
ctx.logger.info("Starting find-proposal") *>
|
ctx.logger.info("Starting find-proposal") *>
|
||||||
rmas.traverse(rm => processAttachment(rm, data.findDates(rm), ctx).map(ml => rm.copy(proposals = ml))).
|
rmas
|
||||||
flatMap(rmv => rmv.traverse(rm =>
|
.traverse(rm =>
|
||||||
|
processAttachment(rm, data.findDates(rm), ctx).map(ml => rm.copy(proposals = ml))
|
||||||
|
)
|
||||||
|
.flatMap(rmv =>
|
||||||
|
rmv
|
||||||
|
.traverse(rm =>
|
||||||
ctx.logger.debug(s"Storing attachment proposals: ${rm.proposals}") *>
|
ctx.logger.debug(s"Storing attachment proposals: ${rm.proposals}") *>
|
||||||
ctx.store.transact(RAttachmentMeta.updateProposals(rm.id, rm.proposals))).
|
ctx.store.transact(RAttachmentMeta.updateProposals(rm.id, rm.proposals))
|
||||||
map(_ => data.copy(metas = rmv)))
|
)
|
||||||
|
.map(_ => data.copy(metas = rmv))
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
def processAttachment[F[_]: Sync]( rm: RAttachmentMeta
|
def processAttachment[F[_]: Sync](
|
||||||
, rd: Vector[NerDateLabel]
|
rm: RAttachmentMeta,
|
||||||
, ctx: Context[F, ProcessItemArgs]): F[MetaProposalList] = {
|
rd: Vector[NerDateLabel],
|
||||||
|
ctx: Context[F, ProcessItemArgs]
|
||||||
|
): F[MetaProposalList] = {
|
||||||
val finder = Finder.searchExact(ctx).next(Finder.searchFuzzy(ctx))
|
val finder = Finder.searchExact(ctx).next(Finder.searchFuzzy(ctx))
|
||||||
List(finder.find(rm.nerlabels), makeDateProposal(rd)).
|
List(finder.find(rm.nerlabels), makeDateProposal(rd))
|
||||||
traverse(identity).map(MetaProposalList.flatten)
|
.traverse(identity)
|
||||||
|
.map(MetaProposalList.flatten)
|
||||||
}
|
}
|
||||||
|
|
||||||
def makeDateProposal[F[_]: Sync](dates: Vector[NerDateLabel]): F[MetaProposalList] = {
|
def makeDateProposal[F[_]: Sync](dates: Vector[NerDateLabel]): F[MetaProposalList] =
|
||||||
Timestamp.current[F].map { now =>
|
Timestamp.current[F].map { now =>
|
||||||
val latestFirst = dates.sortWith(_.date isAfter _.date)
|
val latestFirst = dates.sortWith((l1, l2) => l1.date.isAfter(l2.date))
|
||||||
val nowDate = now.value.atZone(ZoneId.of("GMT")).toLocalDate
|
val nowDate = now.value.atZone(ZoneId.of("GMT")).toLocalDate
|
||||||
val (after, before) = latestFirst.span(ndl => ndl.date.isAfter(nowDate))
|
val (after, before) = latestFirst.span(ndl => ndl.date.isAfter(nowDate))
|
||||||
|
|
||||||
val dueDates = MetaProposalList.fromSeq1(MetaProposalType.DueDate,
|
val dueDates = MetaProposalList.fromSeq1(
|
||||||
after.map(ndl => Candidate(IdRef(Ident.unsafe(ndl.date.toString), ndl.date.toString), Set(ndl.label))))
|
MetaProposalType.DueDate,
|
||||||
val itemDates = MetaProposalList.fromSeq1(MetaProposalType.DocDate,
|
after.map(ndl =>
|
||||||
before.map(ndl => Candidate(IdRef(Ident.unsafe(ndl.date.toString), ndl.date.toString), Set(ndl.label))))
|
Candidate(IdRef(Ident.unsafe(ndl.date.toString), ndl.date.toString), Set(ndl.label))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
val itemDates = MetaProposalList.fromSeq1(
|
||||||
|
MetaProposalType.DocDate,
|
||||||
|
before.map(ndl =>
|
||||||
|
Candidate(IdRef(Ident.unsafe(ndl.date.toString), ndl.date.toString), Set(ndl.label))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
MetaProposalList.flatten(Seq(dueDates, itemDates))
|
MetaProposalList.flatten(Seq(dueDates, itemDates))
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
def removeDuplicates(labels: List[NerLabel]): List[NerLabel] =
|
def removeDuplicates(labels: List[NerLabel]): List[NerLabel] =
|
||||||
labels.foldLeft((Set.empty[String], List.empty[NerLabel])) { case ((seen, result), el) =>
|
labels
|
||||||
if (seen.contains(el.tag.name+el.label.toLowerCase)) (seen, result)
|
.foldLeft((Set.empty[String], List.empty[NerLabel])) {
|
||||||
|
case ((seen, result), el) =>
|
||||||
|
if (seen.contains(el.tag.name + el.label.toLowerCase)) (seen, result)
|
||||||
else (seen + (el.tag.name + el.label.toLowerCase), el :: result)
|
else (seen + (el.tag.name + el.label.toLowerCase), el :: result)
|
||||||
}._2.sortBy(_.startPosition)
|
}
|
||||||
|
._2
|
||||||
|
.sortBy(_.startPosition)
|
||||||
|
|
||||||
trait Finder[F[_]] { self =>
|
trait Finder[F[_]] { self =>
|
||||||
def find(labels: Seq[NerLabel]): F[MetaProposalList]
|
def find(labels: Seq[NerLabel]): F[MetaProposalList]
|
||||||
@ -80,12 +100,14 @@ object FindProposal {
|
|||||||
else f.map(ml1 => ml0.fillEmptyFrom(ml1))
|
else f.map(ml1 => ml0.fillEmptyFrom(ml1))
|
||||||
})
|
})
|
||||||
|
|
||||||
def nextWhenEmpty(f: Finder[F], mt0: MetaProposalType, mts: MetaProposalType*)
|
def nextWhenEmpty(f: Finder[F], mt0: MetaProposalType, mts: MetaProposalType*)(
|
||||||
(implicit F: FlatMap[F], F2: Applicative[F]): Finder[F] =
|
implicit F: FlatMap[F],
|
||||||
flatMap(res0 => {
|
F2: Applicative[F]
|
||||||
|
): Finder[F] =
|
||||||
|
flatMap { res0 =>
|
||||||
if (res0.hasResults(mt0, mts: _*)) Finder.unit[F](res0)
|
if (res0.hasResults(mt0, mts: _*)) Finder.unit[F](res0)
|
||||||
else f.map(res1 => res0.fillEmptyFrom(res1))
|
else f.map(res1 => res0.fillEmptyFrom(res1))
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
object Finder {
|
object Finder {
|
||||||
@ -102,7 +124,11 @@ object FindProposal {
|
|||||||
labels => labels.toList.traverse(nl => search(nl, false, ctx)).map(MetaProposalList.flatten)
|
labels => labels.toList.traverse(nl => search(nl, false, ctx)).map(MetaProposalList.flatten)
|
||||||
}
|
}
|
||||||
|
|
||||||
private def search[F[_]: Sync](nt: NerLabel, exact: Boolean, ctx: Context[F, ProcessItemArgs]): F[MetaProposalList] = {
|
private def search[F[_]: Sync](
|
||||||
|
nt: NerLabel,
|
||||||
|
exact: Boolean,
|
||||||
|
ctx: Context[F, ProcessItemArgs]
|
||||||
|
): F[MetaProposalList] = {
|
||||||
val value =
|
val value =
|
||||||
if (exact) normalizeSearchValue(nt.label)
|
if (exact) normalizeSearchValue(nt.label)
|
||||||
else s"%${normalizeSearchValue(nt.label)}%"
|
else s"%${normalizeSearchValue(nt.label)}%"
|
||||||
@ -110,42 +136,51 @@ object FindProposal {
|
|||||||
if (exact) 2 else 5
|
if (exact) 2 else 5
|
||||||
|
|
||||||
if (value.length < minLength) {
|
if (value.length < minLength) {
|
||||||
ctx.logger.debug(s"Skipping too small value '$value' (original '${nt.label}').").map(_ => MetaProposalList.empty)
|
ctx.logger
|
||||||
} else nt.tag match {
|
.debug(s"Skipping too small value '$value' (original '${nt.label}').")
|
||||||
|
.map(_ => MetaProposalList.empty)
|
||||||
|
} else
|
||||||
|
nt.tag match {
|
||||||
case NerTag.Organization =>
|
case NerTag.Organization =>
|
||||||
ctx.logger.debug(s"Looking for organizations: $value") *>
|
ctx.logger.debug(s"Looking for organizations: $value") *>
|
||||||
ctx.store.transact(ROrganization.findLike(ctx.args.meta.collective, value)).
|
ctx.store
|
||||||
map(MetaProposalList.from(MetaProposalType.CorrOrg, nt))
|
.transact(ROrganization.findLike(ctx.args.meta.collective, value))
|
||||||
|
.map(MetaProposalList.from(MetaProposalType.CorrOrg, nt))
|
||||||
|
|
||||||
case NerTag.Person =>
|
case NerTag.Person =>
|
||||||
val s1 = ctx.store.transact(RPerson.findLike(ctx.args.meta.collective, value, true)).
|
val s1 = ctx.store
|
||||||
map(MetaProposalList.from(MetaProposalType.ConcPerson, nt))
|
.transact(RPerson.findLike(ctx.args.meta.collective, value, true))
|
||||||
val s2 = ctx.store.transact(RPerson.findLike(ctx.args.meta.collective, value, false)).
|
.map(MetaProposalList.from(MetaProposalType.ConcPerson, nt))
|
||||||
map(MetaProposalList.from(MetaProposalType.CorrPerson, nt))
|
val s2 = ctx.store
|
||||||
|
.transact(RPerson.findLike(ctx.args.meta.collective, value, false))
|
||||||
|
.map(MetaProposalList.from(MetaProposalType.CorrPerson, nt))
|
||||||
ctx.logger.debug(s"Looking for persons: $value") *> (for {
|
ctx.logger.debug(s"Looking for persons: $value") *> (for {
|
||||||
ml0 <- s1
|
ml0 <- s1
|
||||||
ml1 <- s2
|
ml1 <- s2
|
||||||
} yield ml0 |+| ml1)
|
} yield ml0 |+| ml1)
|
||||||
|
|
||||||
case NerTag.Location =>
|
case NerTag.Location =>
|
||||||
ctx.logger.debug(s"NerTag 'Location' is currently not used. Ignoring value '$value'.").
|
ctx.logger
|
||||||
map(_ => MetaProposalList.empty)
|
.debug(s"NerTag 'Location' is currently not used. Ignoring value '$value'.")
|
||||||
|
.map(_ => MetaProposalList.empty)
|
||||||
|
|
||||||
case NerTag.Misc =>
|
case NerTag.Misc =>
|
||||||
ctx.logger.debug(s"Looking for equipments: $value") *>
|
ctx.logger.debug(s"Looking for equipments: $value") *>
|
||||||
ctx.store.transact(REquipment.findLike(ctx.args.meta.collective, value)).
|
ctx.store
|
||||||
map(MetaProposalList.from(MetaProposalType.ConcEquip, nt))
|
.transact(REquipment.findLike(ctx.args.meta.collective, value))
|
||||||
|
.map(MetaProposalList.from(MetaProposalType.ConcEquip, nt))
|
||||||
|
|
||||||
case NerTag.Email =>
|
case NerTag.Email =>
|
||||||
searchContact(nt, ContactKind.Email, value, ctx)
|
searchContact(nt, ContactKind.Email, value, ctx)
|
||||||
|
|
||||||
case NerTag.Website =>
|
case NerTag.Website =>
|
||||||
if (!exact) {
|
if (!exact) {
|
||||||
val searchString = Domain.domainFromUri(nt.label.toLowerCase).
|
val searchString = Domain
|
||||||
toOption.
|
.domainFromUri(nt.label.toLowerCase)
|
||||||
map(_.toPrimaryDomain.asString).
|
.toOption
|
||||||
map(s => s"%$s%").
|
.map(_.toPrimaryDomain.asString)
|
||||||
getOrElse(value)
|
.map(s => s"%$s%")
|
||||||
|
.getOrElse(value)
|
||||||
searchContact(nt, ContactKind.Website, searchString, ctx)
|
searchContact(nt, ContactKind.Website, searchString, ctx)
|
||||||
} else {
|
} else {
|
||||||
searchContact(nt, ContactKind.Website, value, ctx)
|
searchContact(nt, ContactKind.Website, value, ctx)
|
||||||
@ -157,23 +192,28 @@ object FindProposal {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private def searchContact[F[_]: Sync]( nt: NerLabel
|
private def searchContact[F[_]: Sync](
|
||||||
, kind: ContactKind
|
nt: NerLabel,
|
||||||
, value: String
|
kind: ContactKind,
|
||||||
, ctx: Context[F, ProcessItemArgs]): F[MetaProposalList] = {
|
value: String,
|
||||||
val orgs = ctx.store.transact(ROrganization.findLike(ctx.args.meta.collective, kind, value)).
|
ctx: Context[F, ProcessItemArgs]
|
||||||
map(MetaProposalList.from(MetaProposalType.CorrOrg, nt))
|
): F[MetaProposalList] = {
|
||||||
val corrP = ctx.store.transact(RPerson.findLike(ctx.args.meta.collective, kind, value, false)).
|
val orgs = ctx.store
|
||||||
map(MetaProposalList.from(MetaProposalType.CorrPerson, nt))
|
.transact(ROrganization.findLike(ctx.args.meta.collective, kind, value))
|
||||||
val concP = ctx.store.transact(RPerson.findLike(ctx.args.meta.collective, kind, value, true)).
|
.map(MetaProposalList.from(MetaProposalType.CorrOrg, nt))
|
||||||
map(MetaProposalList.from(MetaProposalType.CorrPerson, nt))
|
val corrP = ctx.store
|
||||||
|
.transact(RPerson.findLike(ctx.args.meta.collective, kind, value, false))
|
||||||
|
.map(MetaProposalList.from(MetaProposalType.CorrPerson, nt))
|
||||||
|
val concP = ctx.store
|
||||||
|
.transact(RPerson.findLike(ctx.args.meta.collective, kind, value, true))
|
||||||
|
.map(MetaProposalList.from(MetaProposalType.CorrPerson, nt))
|
||||||
|
|
||||||
ctx.logger.debug(s"Looking with $kind: $value") *>
|
ctx.logger.debug(s"Looking with $kind: $value") *>
|
||||||
List(orgs, corrP, concP).traverse(identity).map(MetaProposalList.flatten)
|
List(orgs, corrP, concP).traverse(identity).map(MetaProposalList.flatten)
|
||||||
}
|
}
|
||||||
|
|
||||||
// The backslash *must* be stripped from search strings.
|
// The backslash *must* be stripped from search strings.
|
||||||
private [this] val invalidSearch =
|
private[this] val invalidSearch =
|
||||||
"…_[]^<>=&ſ/{}*?@#$|~`+%\"';\\".toSet
|
"…_[]^<>=&ſ/{}*?@#$|~`+%\"';\\".toSet
|
||||||
|
|
||||||
private def normalizeSearchValue(str: String): String =
|
private def normalizeSearchValue(str: String): String =
|
||||||
|
@ -4,10 +4,12 @@ import docspell.common.{Ident, NerDateLabel, NerLabel}
|
|||||||
import docspell.joex.process.ItemData.AttachmentDates
|
import docspell.joex.process.ItemData.AttachmentDates
|
||||||
import docspell.store.records.{RAttachment, RAttachmentMeta, RItem}
|
import docspell.store.records.{RAttachment, RAttachmentMeta, RItem}
|
||||||
|
|
||||||
case class ItemData( item: RItem
|
case class ItemData(
|
||||||
, attachments: Vector[RAttachment]
|
item: RItem,
|
||||||
, metas: Vector[RAttachmentMeta]
|
attachments: Vector[RAttachment],
|
||||||
, dateLabels: Vector[AttachmentDates]) {
|
metas: Vector[RAttachmentMeta],
|
||||||
|
dateLabels: Vector[AttachmentDates]
|
||||||
|
) {
|
||||||
|
|
||||||
def findMeta(attachId: Ident): Option[RAttachmentMeta] =
|
def findMeta(attachId: Ident): Option[RAttachmentMeta] =
|
||||||
metas.find(_.id == attachId)
|
metas.find(_.id == attachId)
|
||||||
@ -16,7 +18,6 @@ case class ItemData( item: RItem
|
|||||||
dateLabels.find(m => m.rm.id == rm.id).map(_.dates).getOrElse(Vector.empty)
|
dateLabels.find(m => m.rm.id == rm.id).map(_.dates).getOrElse(Vector.empty)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
object ItemData {
|
object ItemData {
|
||||||
|
|
||||||
case class AttachmentDates(rm: RAttachmentMeta, dates: Vector[NerDateLabel]) {
|
case class AttachmentDates(rm: RAttachmentMeta, dates: Vector[NerDateLabel]) {
|
||||||
|
@ -10,14 +10,13 @@ import docspell.text.ocr.{Config => OcrConfig}
|
|||||||
|
|
||||||
object ItemHandler {
|
object ItemHandler {
|
||||||
def onCancel[F[_]: Sync: ContextShift]: Task[F, ProcessItemArgs, Unit] =
|
def onCancel[F[_]: Sync: ContextShift]: Task[F, ProcessItemArgs, Unit] =
|
||||||
logWarn("Now cancelling. Deleting potentially created data.").
|
logWarn("Now cancelling. Deleting potentially created data.").flatMap(_ => deleteByFileIds)
|
||||||
flatMap(_ => deleteByFileIds)
|
|
||||||
|
|
||||||
def apply[F[_]: Sync: ContextShift](cfg: OcrConfig): Task[F, ProcessItemArgs, Unit] =
|
def apply[F[_]: Sync: ContextShift](cfg: OcrConfig): Task[F, ProcessItemArgs, Unit] =
|
||||||
CreateItem[F].
|
CreateItem[F]
|
||||||
flatMap(itemStateTask(ItemState.Processing)).
|
.flatMap(itemStateTask(ItemState.Processing))
|
||||||
flatMap(safeProcess[F](cfg)).
|
.flatMap(safeProcess[F](cfg))
|
||||||
map(_ => ())
|
.map(_ => ())
|
||||||
|
|
||||||
def itemStateTask[F[_]: Sync, A](state: ItemState)(data: ItemData): Task[F, A, ItemData] =
|
def itemStateTask[F[_]: Sync, A](state: ItemState)(data: ItemData): Task[F, A, ItemData] =
|
||||||
Task { ctx =>
|
Task { ctx =>
|
||||||
@ -30,22 +29,21 @@ object ItemHandler {
|
|||||||
last = ctx.config.retries == current.getOrElse(0)
|
last = ctx.config.retries == current.getOrElse(0)
|
||||||
} yield last
|
} yield last
|
||||||
|
|
||||||
|
def safeProcess[F[_]: Sync: ContextShift](
|
||||||
def safeProcess[F[_]: Sync: ContextShift](cfg: OcrConfig)(data: ItemData): Task[F, ProcessItemArgs, ItemData] =
|
cfg: OcrConfig
|
||||||
|
)(data: ItemData): Task[F, ProcessItemArgs, ItemData] =
|
||||||
Task(isLastRetry[F, ProcessItemArgs] _).flatMap {
|
Task(isLastRetry[F, ProcessItemArgs] _).flatMap {
|
||||||
case true =>
|
case true =>
|
||||||
ProcessItem[F](cfg)(data).
|
ProcessItem[F](cfg)(data).attempt.flatMap({
|
||||||
attempt.flatMap({
|
|
||||||
case Right(d) =>
|
case Right(d) =>
|
||||||
Task.pure(d)
|
Task.pure(d)
|
||||||
case Left(ex) =>
|
case Left(ex) =>
|
||||||
logWarn[F]("Processing failed on last retry. Creating item but without proposals.").
|
logWarn[F]("Processing failed on last retry. Creating item but without proposals.")
|
||||||
flatMap(_ => itemStateTask(ItemState.Created)(data)).
|
.flatMap(_ => itemStateTask(ItemState.Created)(data))
|
||||||
andThen(_ => Sync[F].raiseError(ex))
|
.andThen(_ => Sync[F].raiseError(ex))
|
||||||
})
|
})
|
||||||
case false =>
|
case false =>
|
||||||
ProcessItem[F](cfg)(data).
|
ProcessItem[F](cfg)(data).flatMap(itemStateTask(ItemState.Created))
|
||||||
flatMap(itemStateTask(ItemState.Created))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
def deleteByFileIds[F[_]: Sync: ContextShift]: Task[F, ProcessItemArgs, Unit] =
|
def deleteByFileIds[F[_]: Sync: ContextShift]: Task[F, ProcessItemArgs, Unit] =
|
||||||
|
@ -13,28 +13,40 @@ object LinkProposal {
|
|||||||
val proposals = MetaProposalList.flatten(data.metas.map(_.proposals))
|
val proposals = MetaProposalList.flatten(data.metas.map(_.proposals))
|
||||||
|
|
||||||
ctx.logger.info(s"Starting linking proposals") *>
|
ctx.logger.info(s"Starting linking proposals") *>
|
||||||
MetaProposalType.all.
|
MetaProposalType.all
|
||||||
traverse(applyValue(data, proposals, ctx)).
|
.traverse(applyValue(data, proposals, ctx))
|
||||||
map(result => ctx.logger.info(s"Results from proposal processing: $result")).
|
.map(result => ctx.logger.info(s"Results from proposal processing: $result"))
|
||||||
map(_ => data)
|
.map(_ => data)
|
||||||
}
|
}
|
||||||
|
|
||||||
def applyValue[F[_]: Sync](data: ItemData, proposalList: MetaProposalList, ctx: Context[F, ProcessItemArgs])(mpt: MetaProposalType): F[Result] = {
|
def applyValue[F[_]: Sync](
|
||||||
|
data: ItemData,
|
||||||
|
proposalList: MetaProposalList,
|
||||||
|
ctx: Context[F, ProcessItemArgs]
|
||||||
|
)(mpt: MetaProposalType): F[Result] =
|
||||||
proposalList.find(mpt) match {
|
proposalList.find(mpt) match {
|
||||||
case None =>
|
case None =>
|
||||||
Result.noneFound(mpt).pure[F]
|
Result.noneFound(mpt).pure[F]
|
||||||
case Some(a) if a.isSingleValue =>
|
case Some(a) if a.isSingleValue =>
|
||||||
ctx.logger.info(s"Found one candidate for ${a.proposalType}") *>
|
ctx.logger.info(s"Found one candidate for ${a.proposalType}") *>
|
||||||
setItemMeta(data.item.id, ctx, a.proposalType, a.values.head.ref.id).
|
setItemMeta(data.item.id, ctx, a.proposalType, a.values.head.ref.id).map(_ =>
|
||||||
map(_ => Result.single(mpt))
|
Result.single(mpt)
|
||||||
|
)
|
||||||
case Some(a) =>
|
case Some(a) =>
|
||||||
ctx.logger.info(s"Found many (${a.size}, ${a.values.map(_.ref.id.id)}) candidates for ${a.proposalType}. Setting first.") *>
|
ctx.logger.info(
|
||||||
setItemMeta(data.item.id, ctx, a.proposalType, a.values.head.ref.id).
|
s"Found many (${a.size}, ${a.values.map(_.ref.id.id)}) candidates for ${a.proposalType}. Setting first."
|
||||||
map(_ => Result.multiple(mpt))
|
) *>
|
||||||
}
|
setItemMeta(data.item.id, ctx, a.proposalType, a.values.head.ref.id).map(_ =>
|
||||||
|
Result.multiple(mpt)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
def setItemMeta[F[_]: Sync](itemId: Ident, ctx: Context[F, ProcessItemArgs], mpt: MetaProposalType, value: Ident): F[Int] =
|
def setItemMeta[F[_]: Sync](
|
||||||
|
itemId: Ident,
|
||||||
|
ctx: Context[F, ProcessItemArgs],
|
||||||
|
mpt: MetaProposalType,
|
||||||
|
value: Ident
|
||||||
|
): F[Int] =
|
||||||
mpt match {
|
mpt match {
|
||||||
case MetaProposalType.CorrOrg =>
|
case MetaProposalType.CorrOrg =>
|
||||||
ctx.logger.debug(s"Updating item organization with: ${value.id}") *>
|
ctx.logger.debug(s"Updating item organization with: ${value.id}") *>
|
||||||
@ -54,7 +66,6 @@ object LinkProposal {
|
|||||||
ctx.logger.debug(s"Not linking document date suggestion ${value.id}").map(_ => 0)
|
ctx.logger.debug(s"Not linking document date suggestion ${value.id}").map(_ => 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
sealed trait Result {
|
sealed trait Result {
|
||||||
def proposalType: MetaProposalType
|
def proposalType: MetaProposalType
|
||||||
}
|
}
|
||||||
|
@ -7,13 +7,15 @@ import docspell.text.ocr.{Config => OcrConfig}
|
|||||||
|
|
||||||
object ProcessItem {
|
object ProcessItem {
|
||||||
|
|
||||||
def apply[F[_]: Sync: ContextShift](cfg: OcrConfig)(item: ItemData): Task[F, ProcessItemArgs, ItemData] =
|
def apply[F[_]: Sync: ContextShift](
|
||||||
TextExtraction(cfg, item).
|
cfg: OcrConfig
|
||||||
flatMap(Task.setProgress(25)).
|
)(item: ItemData): Task[F, ProcessItemArgs, ItemData] =
|
||||||
flatMap(TextAnalysis[F]).
|
TextExtraction(cfg, item)
|
||||||
flatMap(Task.setProgress(50)).
|
.flatMap(Task.setProgress(25))
|
||||||
flatMap(FindProposal[F]).
|
.flatMap(TextAnalysis[F])
|
||||||
flatMap(Task.setProgress(75)).
|
.flatMap(Task.setProgress(50))
|
||||||
flatMap(LinkProposal[F]).
|
.flatMap(FindProposal[F])
|
||||||
flatMap(Task.setProgress(99))
|
.flatMap(Task.setProgress(75))
|
||||||
|
.flatMap(LinkProposal[F])
|
||||||
|
.flatMap(Task.setProgress(99))
|
||||||
}
|
}
|
||||||
|
@ -8,7 +8,7 @@ import docspell.joex.scheduler.Task
|
|||||||
import org.log4s._
|
import org.log4s._
|
||||||
|
|
||||||
object TestTasks {
|
object TestTasks {
|
||||||
private [this] val logger = getLogger
|
private[this] val logger = getLogger
|
||||||
|
|
||||||
def success[F[_]]: Task[F, ProcessItemArgs, Unit] =
|
def success[F[_]]: Task[F, ProcessItemArgs, Unit] =
|
||||||
Task { ctx =>
|
Task { ctx =>
|
||||||
@ -17,9 +17,9 @@ object TestTasks {
|
|||||||
|
|
||||||
def failing[F[_]: Sync]: Task[F, ProcessItemArgs, Unit] =
|
def failing[F[_]: Sync]: Task[F, ProcessItemArgs, Unit] =
|
||||||
Task { ctx =>
|
Task { ctx =>
|
||||||
ctx.logger.info(s"Failing the task run :(").map(_ =>
|
ctx.logger
|
||||||
sys.error("Oh, cannot extract gold from this document")
|
.info(s"Failing the task run :(")
|
||||||
)
|
.map(_ => sys.error("Oh, cannot extract gold from this document"))
|
||||||
}
|
}
|
||||||
|
|
||||||
def longRunning[F[_]: Sync]: Task[F, ProcessItemArgs, Unit] =
|
def longRunning[F[_]: Sync]: Task[F, ProcessItemArgs, Unit] =
|
||||||
@ -34,6 +34,6 @@ object TestTasks {
|
|||||||
ctx.logger.info("doing more things")
|
ctx.logger.info("doing more things")
|
||||||
}
|
}
|
||||||
|
|
||||||
private def sleep[F[_]:Sync](ms: Long): F[Unit] =
|
private def sleep[F[_]: Sync](ms: Long): F[Unit] =
|
||||||
Sync[F].delay(Thread.sleep(ms))
|
Sync[F].delay(Thread.sleep(ms))
|
||||||
}
|
}
|
||||||
|
@ -19,21 +19,26 @@ object TextAnalysis {
|
|||||||
s <- Duration.stopTime[F]
|
s <- Duration.stopTime[F]
|
||||||
t <- item.metas.toList.traverse(annotateAttachment[F](ctx.args.meta.language))
|
t <- item.metas.toList.traverse(annotateAttachment[F](ctx.args.meta.language))
|
||||||
_ <- ctx.logger.debug(s"Storing tags: ${t.map(_._1.copy(content = None))}")
|
_ <- ctx.logger.debug(s"Storing tags: ${t.map(_._1.copy(content = None))}")
|
||||||
_ <- t.traverse(m => ctx.store.transact(RAttachmentMeta.updateLabels(m._1.id, m._1.nerlabels)))
|
_ <- t.traverse(m =>
|
||||||
|
ctx.store.transact(RAttachmentMeta.updateLabels(m._1.id, m._1.nerlabels))
|
||||||
|
)
|
||||||
e <- s
|
e <- s
|
||||||
_ <- ctx.logger.info(s"Text-Analysis finished in ${e.formatExact}")
|
_ <- ctx.logger.info(s"Text-Analysis finished in ${e.formatExact}")
|
||||||
v = t.toVector
|
v = t.toVector
|
||||||
} yield item.copy(metas = v.map(_._1), dateLabels = v.map(_._2))
|
} yield item.copy(metas = v.map(_._1), dateLabels = v.map(_._2))
|
||||||
}
|
}
|
||||||
|
|
||||||
def annotateAttachment[F[_]: Sync](lang: Language)(rm: RAttachmentMeta): F[(RAttachmentMeta, AttachmentDates)] =
|
def annotateAttachment[F[_]: Sync](
|
||||||
|
lang: Language
|
||||||
|
)(rm: RAttachmentMeta): F[(RAttachmentMeta, AttachmentDates)] =
|
||||||
for {
|
for {
|
||||||
list0 <- stanfordNer[F](lang, rm)
|
list0 <- stanfordNer[F](lang, rm)
|
||||||
list1 <- contactNer[F](rm)
|
list1 <- contactNer[F](rm)
|
||||||
dates <- dateNer[F](rm, lang)
|
dates <- dateNer[F](rm, lang)
|
||||||
} yield (rm.copy(nerlabels = (list0 ++ list1 ++ dates.toNerLabel).toList), dates)
|
} yield (rm.copy(nerlabels = (list0 ++ list1 ++ dates.toNerLabel).toList), dates)
|
||||||
|
|
||||||
def stanfordNer[F[_]: Sync](lang: Language, rm: RAttachmentMeta): F[Vector[NerLabel]] = Sync[F].delay {
|
def stanfordNer[F[_]: Sync](lang: Language, rm: RAttachmentMeta): F[Vector[NerLabel]] =
|
||||||
|
Sync[F].delay {
|
||||||
rm.content.map(StanfordNerClassifier.nerAnnotate(lang)).getOrElse(Vector.empty)
|
rm.content.map(StanfordNerClassifier.nerAnnotate(lang)).getOrElse(Vector.empty)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -42,8 +47,10 @@ object TextAnalysis {
|
|||||||
}
|
}
|
||||||
|
|
||||||
def dateNer[F[_]: Sync](rm: RAttachmentMeta, lang: Language): F[AttachmentDates] = Sync[F].delay {
|
def dateNer[F[_]: Sync](rm: RAttachmentMeta, lang: Language): F[AttachmentDates] = Sync[F].delay {
|
||||||
AttachmentDates(rm, rm.content.map(txt => DateFind.findDates(txt, lang).toVector).getOrElse(Vector.empty))
|
AttachmentDates(
|
||||||
|
rm,
|
||||||
|
rm.content.map(txt => DateFind.findDates(txt, lang).toVector).getOrElse(Vector.empty)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -11,7 +11,10 @@ import docspell.text.ocr.{TextExtract, Config => OcrConfig}
|
|||||||
|
|
||||||
object TextExtraction {
|
object TextExtraction {
|
||||||
|
|
||||||
def apply[F[_]: Sync : ContextShift](cfg: OcrConfig, item: ItemData): Task[F, ProcessItemArgs, ItemData] =
|
def apply[F[_]: Sync: ContextShift](
|
||||||
|
cfg: OcrConfig,
|
||||||
|
item: ItemData
|
||||||
|
): Task[F, ProcessItemArgs, ItemData] =
|
||||||
Task { ctx =>
|
Task { ctx =>
|
||||||
for {
|
for {
|
||||||
_ <- ctx.logger.info("Starting text extraction")
|
_ <- ctx.logger.info("Starting text extraction")
|
||||||
@ -24,22 +27,33 @@ object TextExtraction {
|
|||||||
} yield item.copy(metas = txt)
|
} yield item.copy(metas = txt)
|
||||||
}
|
}
|
||||||
|
|
||||||
def extractTextToMeta[F[_]: Sync : ContextShift](ctx: Context[F, _], cfg: OcrConfig, lang: Language)(ra: RAttachment): F[RAttachmentMeta] =
|
def extractTextToMeta[F[_]: Sync: ContextShift](
|
||||||
|
ctx: Context[F, _],
|
||||||
|
cfg: OcrConfig,
|
||||||
|
lang: Language
|
||||||
|
)(ra: RAttachment): F[RAttachmentMeta] =
|
||||||
for {
|
for {
|
||||||
_ <- ctx.logger.debug(s"Extracting text for attachment ${ra.name}")
|
_ <- ctx.logger.debug(s"Extracting text for attachment ${ra.name}")
|
||||||
dst <- Duration.stopTime[F]
|
dst <- Duration.stopTime[F]
|
||||||
txt <- extractText(cfg, lang, ctx.store, ctx.blocker)(ra)
|
txt <- extractText(cfg, lang, ctx.store, ctx.blocker)(ra)
|
||||||
meta = RAttachmentMeta.empty(ra.id).copy(content = txt.map(_.trim).filter(_.nonEmpty))
|
meta = RAttachmentMeta.empty(ra.id).copy(content = txt.map(_.trim).filter(_.nonEmpty))
|
||||||
est <- dst
|
est <- dst
|
||||||
_ <- ctx.logger.debug(s"Extracting text for attachment ${ra.name} finished in ${est.formatExact}")
|
_ <- ctx.logger.debug(
|
||||||
|
s"Extracting text for attachment ${ra.name} finished in ${est.formatExact}"
|
||||||
|
)
|
||||||
} yield meta
|
} yield meta
|
||||||
|
|
||||||
def extractText[F[_]: Sync : ContextShift](ocrConfig: OcrConfig, lang: Language, store: Store[F], blocker: Blocker)(ra: RAttachment): F[Option[String]] = {
|
def extractText[F[_]: Sync: ContextShift](
|
||||||
val data = store.bitpeace.get(ra.fileId.id).
|
ocrConfig: OcrConfig,
|
||||||
unNoneTerminate.
|
lang: Language,
|
||||||
through(store.bitpeace.fetchData2(RangeDef.all))
|
store: Store[F],
|
||||||
|
blocker: Blocker
|
||||||
|
)(ra: RAttachment): F[Option[String]] = {
|
||||||
|
val data = store.bitpeace
|
||||||
|
.get(ra.fileId.id)
|
||||||
|
.unNoneTerminate
|
||||||
|
.through(store.bitpeace.fetchData2(RangeDef.all))
|
||||||
|
|
||||||
TextExtract.extract(data, blocker, lang.iso3, ocrConfig).
|
TextExtract.extract(data, blocker, lang.iso3, ocrConfig).compile.last
|
||||||
compile.last
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -10,15 +10,19 @@ import org.http4s.dsl.Http4sDsl
|
|||||||
object InfoRoutes {
|
object InfoRoutes {
|
||||||
|
|
||||||
def apply[F[_]: Sync](): HttpRoutes[F] = {
|
def apply[F[_]: Sync](): HttpRoutes[F] = {
|
||||||
val dsl = new Http4sDsl[F]{}
|
val dsl = new Http4sDsl[F] {}
|
||||||
import dsl._
|
import dsl._
|
||||||
HttpRoutes.of[F] {
|
HttpRoutes.of[F] {
|
||||||
case GET -> (Root / "version") =>
|
case GET -> (Root / "version") =>
|
||||||
Ok(VersionInfo(BuildInfo.version
|
Ok(
|
||||||
, BuildInfo.builtAtMillis
|
VersionInfo(
|
||||||
, BuildInfo.builtAtString
|
BuildInfo.version,
|
||||||
, BuildInfo.gitHeadCommit.getOrElse("")
|
BuildInfo.builtAtMillis,
|
||||||
, BuildInfo.gitDescribedVersion.getOrElse("")))
|
BuildInfo.builtAtString,
|
||||||
|
BuildInfo.gitHeadCommit.getOrElse(""),
|
||||||
|
BuildInfo.gitDescribedVersion.getOrElse("")
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -13,7 +13,7 @@ import org.http4s.dsl.Http4sDsl
|
|||||||
object JoexRoutes {
|
object JoexRoutes {
|
||||||
|
|
||||||
def apply[F[_]: ConcurrentEffect: Timer](app: JoexApp[F]): HttpRoutes[F] = {
|
def apply[F[_]: ConcurrentEffect: Timer](app: JoexApp[F]): HttpRoutes[F] = {
|
||||||
val dsl = new Http4sDsl[F]{}
|
val dsl = new Http4sDsl[F] {}
|
||||||
import dsl._
|
import dsl._
|
||||||
HttpRoutes.of[F] {
|
HttpRoutes.of[F] {
|
||||||
case POST -> Root / "notify" =>
|
case POST -> Root / "notify" =>
|
||||||
@ -31,7 +31,9 @@ object JoexRoutes {
|
|||||||
|
|
||||||
case POST -> Root / "shutdownAndExit" =>
|
case POST -> Root / "shutdownAndExit" =>
|
||||||
for {
|
for {
|
||||||
_ <- ConcurrentEffect[F].start(Timer[F].sleep(Duration.seconds(1).toScala) *> app.initShutdown)
|
_ <- ConcurrentEffect[F].start(
|
||||||
|
Timer[F].sleep(Duration.seconds(1).toScala) *> app.initShutdown
|
||||||
|
)
|
||||||
resp <- Ok(BasicResult(true, "Shutdown initiated."))
|
resp <- Ok(BasicResult(true, "Shutdown initiated."))
|
||||||
} yield resp
|
} yield resp
|
||||||
|
|
||||||
@ -52,7 +54,15 @@ object JoexRoutes {
|
|||||||
}
|
}
|
||||||
|
|
||||||
def mkJob(j: RJob): Job =
|
def mkJob(j: RJob): Job =
|
||||||
Job(j.id, j.subject, j.submitted, j.priority, j.retries, j.progress, j.started.getOrElse(Timestamp.Epoch))
|
Job(
|
||||||
|
j.id,
|
||||||
|
j.subject,
|
||||||
|
j.submitted,
|
||||||
|
j.priority,
|
||||||
|
j.retries,
|
||||||
|
j.progress,
|
||||||
|
j.started.getOrElse(Timestamp.Epoch)
|
||||||
|
)
|
||||||
|
|
||||||
def mkJobLog(j: RJob, jl: Vector[RJobLog]): JobAndLog =
|
def mkJobLog(j: RJob, jl: Vector[RJobLog]): JobAndLog =
|
||||||
JobAndLog(mkJob(j), jl.map(r => JobLogEvent(r.created, r.level, r.message)).toList)
|
JobAndLog(mkJob(j), jl.map(r => JobLogEvent(r.created, r.level, r.message)).toList)
|
||||||
|
@ -30,22 +30,26 @@ trait Context[F[_], A] { self =>
|
|||||||
}
|
}
|
||||||
|
|
||||||
object Context {
|
object Context {
|
||||||
private [this] val log = getLogger
|
private[this] val log = getLogger
|
||||||
|
|
||||||
def create[F[_]: Functor, A]( job: RJob
|
def create[F[_]: Functor, A](
|
||||||
, arg: A
|
job: RJob,
|
||||||
, config: SchedulerConfig
|
arg: A,
|
||||||
, log: Logger[F]
|
config: SchedulerConfig,
|
||||||
, store: Store[F]
|
log: Logger[F],
|
||||||
, blocker: Blocker): Context[F, A] =
|
store: Store[F],
|
||||||
|
blocker: Blocker
|
||||||
|
): Context[F, A] =
|
||||||
new ContextImpl(arg, log, store, blocker, config, job.id)
|
new ContextImpl(arg, log, store, blocker, config, job.id)
|
||||||
|
|
||||||
def apply[F[_]: Concurrent, A]( job: RJob
|
def apply[F[_]: Concurrent, A](
|
||||||
, arg: A
|
job: RJob,
|
||||||
, config: SchedulerConfig
|
arg: A,
|
||||||
, logSink: LogSink[F]
|
config: SchedulerConfig,
|
||||||
, blocker: Blocker
|
logSink: LogSink[F],
|
||||||
, store: Store[F]): F[Context[F, A]] =
|
blocker: Blocker,
|
||||||
|
store: Store[F]
|
||||||
|
): F[Context[F, A]] =
|
||||||
for {
|
for {
|
||||||
_ <- log.ftrace("Creating logger for task run")
|
_ <- log.ftrace("Creating logger for task run")
|
||||||
logger <- Logger(job.id, job.info, config.logBufferSize, logSink)
|
logger <- Logger(job.id, job.info, config.logBufferSize, logSink)
|
||||||
@ -53,13 +57,14 @@ object Context {
|
|||||||
ctx = create[F, A](job, arg, config, logger, store, blocker)
|
ctx = create[F, A](job, arg, config, logger, store, blocker)
|
||||||
} yield ctx
|
} yield ctx
|
||||||
|
|
||||||
private final class ContextImpl[F[_]: Functor, A]( val args: A
|
final private class ContextImpl[F[_]: Functor, A](
|
||||||
, val logger: Logger[F]
|
val args: A,
|
||||||
, val store: Store[F]
|
val logger: Logger[F],
|
||||||
, val blocker: Blocker
|
val store: Store[F],
|
||||||
, val config: SchedulerConfig
|
val blocker: Blocker,
|
||||||
, val jobId: Ident)
|
val config: SchedulerConfig,
|
||||||
extends Context[F,A] {
|
val jobId: Ident
|
||||||
|
) extends Context[F, A] {
|
||||||
|
|
||||||
def setProgress(percent: Int): F[Unit] = {
|
def setProgress(percent: Int): F[Unit] = {
|
||||||
val pval = math.min(100, math.max(0, percent))
|
val pval = math.min(100, math.max(0, percent))
|
||||||
|
@ -11,14 +11,13 @@ import docspell.common.Priority
|
|||||||
*/
|
*/
|
||||||
case class CountingScheme(high: Int, low: Int, counter: Int = 0) {
|
case class CountingScheme(high: Int, low: Int, counter: Int = 0) {
|
||||||
|
|
||||||
def nextPriority: (CountingScheme, Priority) = {
|
def nextPriority: (CountingScheme, Priority) =
|
||||||
if (counter <= 0) (increment, Priority.High)
|
if (counter <= 0) (increment, Priority.High)
|
||||||
else {
|
else {
|
||||||
val rest = counter % (high + low)
|
val rest = counter % (high + low)
|
||||||
if (rest < high) (increment, Priority.High)
|
if (rest < high) (increment, Priority.High)
|
||||||
else (increment, Priority.Low)
|
else (increment, Priority.Low)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
def increment: CountingScheme =
|
def increment: CountingScheme =
|
||||||
copy(counter = counter + 1)
|
copy(counter = counter + 1)
|
||||||
@ -32,8 +31,7 @@ object CountingScheme {
|
|||||||
def readString(str: String): Either[String, CountingScheme] =
|
def readString(str: String): Either[String, CountingScheme] =
|
||||||
str.split(',') match {
|
str.split(',') match {
|
||||||
case Array(h, l) =>
|
case Array(h, l) =>
|
||||||
Either.catchNonFatal(CountingScheme(h.toInt, l.toInt)).
|
Either.catchNonFatal(CountingScheme(h.toInt, l.toInt)).left.map(_.getMessage)
|
||||||
left.map(_.getMessage)
|
|
||||||
case _ =>
|
case _ =>
|
||||||
Left(s"Invalid counting scheme: $str")
|
Left(s"Invalid counting scheme: $str")
|
||||||
}
|
}
|
||||||
|
@ -20,12 +20,15 @@ case class JobTask[F[_]](name: Ident, task: Task[F, String, Unit], onCancel: Tas
|
|||||||
|
|
||||||
object JobTask {
|
object JobTask {
|
||||||
|
|
||||||
def json[F[_]: Sync, A](name: Ident, task: Task[F, A, Unit], onCancel: Task[F, A, Unit])
|
def json[F[_]: Sync, A](name: Ident, task: Task[F, A, Unit], onCancel: Task[F, A, Unit])(
|
||||||
(implicit D: Decoder[A]): JobTask[F] = {
|
implicit D: Decoder[A]
|
||||||
|
): JobTask[F] = {
|
||||||
val convert: String => F[A] =
|
val convert: String => F[A] =
|
||||||
str => str.parseJsonAs[A] match {
|
str =>
|
||||||
|
str.parseJsonAs[A] match {
|
||||||
case Right(a) => a.pure[F]
|
case Right(a) => a.pure[F]
|
||||||
case Left(ex) => Sync[F].raiseError(new Exception(s"Cannot parse task arguments: $str", ex))
|
case Left(ex) =>
|
||||||
|
Sync[F].raiseError(new Exception(s"Cannot parse task arguments: $str", ex))
|
||||||
}
|
}
|
||||||
|
|
||||||
JobTask(name, task.contramap(convert), onCancel.contramap(convert))
|
JobTask(name, task.contramap(convert), onCancel.contramap(convert))
|
||||||
|
@ -4,12 +4,14 @@ import cats.implicits._
|
|||||||
import docspell.common._
|
import docspell.common._
|
||||||
import cats.effect.Sync
|
import cats.effect.Sync
|
||||||
|
|
||||||
case class LogEvent( jobId: Ident
|
case class LogEvent(
|
||||||
, jobInfo: String
|
jobId: Ident,
|
||||||
, time: Timestamp
|
jobInfo: String,
|
||||||
, level: LogLevel
|
time: Timestamp,
|
||||||
, msg: String
|
level: LogLevel,
|
||||||
, ex: Option[Throwable] = None) {
|
msg: String,
|
||||||
|
ex: Option[Throwable] = None
|
||||||
|
) {
|
||||||
|
|
||||||
def logLine: String =
|
def logLine: String =
|
||||||
s">>> ${time.asString} $level $jobInfo: $msg"
|
s">>> ${time.asString} $level $jobInfo: $msg"
|
||||||
@ -21,5 +23,4 @@ object LogEvent {
|
|||||||
def create[F[_]: Sync](jobId: Ident, jobInfo: String, level: LogLevel, msg: String): F[LogEvent] =
|
def create[F[_]: Sync](jobId: Ident, jobInfo: String, level: LogLevel, msg: String): F[LogEvent] =
|
||||||
Timestamp.current[F].map(now => LogEvent(jobId, jobInfo, now, level, msg))
|
Timestamp.current[F].map(now => LogEvent(jobId, jobInfo, now, level, msg))
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -44,12 +44,22 @@ object LogSink {
|
|||||||
LogSink(_.evalMap(e => logInternal(e)))
|
LogSink(_.evalMap(e => logInternal(e)))
|
||||||
|
|
||||||
def db[F[_]: Sync](store: Store[F]): LogSink[F] =
|
def db[F[_]: Sync](store: Store[F]): LogSink[F] =
|
||||||
LogSink(_.evalMap(ev => for {
|
LogSink(
|
||||||
|
_.evalMap(ev =>
|
||||||
|
for {
|
||||||
id <- Ident.randomId[F]
|
id <- Ident.randomId[F]
|
||||||
joblog = RJobLog(id, ev.jobId, ev.level, ev.time, ev.msg + ev.ex.map(th => ": "+ th.getMessage).getOrElse(""))
|
joblog = RJobLog(
|
||||||
|
id,
|
||||||
|
ev.jobId,
|
||||||
|
ev.level,
|
||||||
|
ev.time,
|
||||||
|
ev.msg + ev.ex.map(th => ": " + th.getMessage).getOrElse("")
|
||||||
|
)
|
||||||
_ <- logInternal(ev)
|
_ <- logInternal(ev)
|
||||||
_ <- store.transact(RJobLog.insert(joblog))
|
_ <- store.transact(RJobLog.insert(joblog))
|
||||||
} yield ()))
|
} yield ()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
def dbAndLog[F[_]: Concurrent](store: Store[F]): LogSink[F] = {
|
def dbAndLog[F[_]: Concurrent](store: Store[F]): LogSink[F] = {
|
||||||
val s: Stream[F, Pipe[F, LogEvent, Unit]] =
|
val s: Stream[F, Pipe[F, LogEvent, Unit]] =
|
||||||
|
@ -33,13 +33,21 @@ object Logger {
|
|||||||
LogEvent.create[F](jobId, jobInfo, LogLevel.Warn, msg).flatMap(q.enqueue1)
|
LogEvent.create[F](jobId, jobInfo, LogLevel.Warn, msg).flatMap(q.enqueue1)
|
||||||
|
|
||||||
def error(ex: Throwable)(msg: => String): F[Unit] =
|
def error(ex: Throwable)(msg: => String): F[Unit] =
|
||||||
LogEvent.create[F](jobId, jobInfo, LogLevel.Error, msg).map(le => le.copy(ex = Some(ex))).flatMap(q.enqueue1)
|
LogEvent
|
||||||
|
.create[F](jobId, jobInfo, LogLevel.Error, msg)
|
||||||
|
.map(le => le.copy(ex = Some(ex)))
|
||||||
|
.flatMap(q.enqueue1)
|
||||||
|
|
||||||
def error(msg: => String): F[Unit] =
|
def error(msg: => String): F[Unit] =
|
||||||
LogEvent.create[F](jobId, jobInfo, LogLevel.Error, msg).flatMap(q.enqueue1)
|
LogEvent.create[F](jobId, jobInfo, LogLevel.Error, msg).flatMap(q.enqueue1)
|
||||||
}
|
}
|
||||||
|
|
||||||
def apply[F[_]: Concurrent](jobId: Ident, jobInfo: String, bufferSize: Int, sink: LogSink[F]): F[Logger[F]] =
|
def apply[F[_]: Concurrent](
|
||||||
|
jobId: Ident,
|
||||||
|
jobInfo: String,
|
||||||
|
bufferSize: Int,
|
||||||
|
sink: LogSink[F]
|
||||||
|
): F[Logger[F]] =
|
||||||
for {
|
for {
|
||||||
q <- Queue.circularBuffer[F, LogEvent](bufferSize)
|
q <- Queue.circularBuffer[F, LogEvent](bufferSize)
|
||||||
log = create(jobId, jobInfo, q)
|
log = create(jobId, jobInfo, q)
|
||||||
|
@ -7,13 +7,14 @@ import docspell.store.Store
|
|||||||
import docspell.store.queue.JobQueue
|
import docspell.store.queue.JobQueue
|
||||||
import fs2.concurrent.SignallingRef
|
import fs2.concurrent.SignallingRef
|
||||||
|
|
||||||
case class SchedulerBuilder[F[_]: ConcurrentEffect : ContextShift](
|
case class SchedulerBuilder[F[_]: ConcurrentEffect: ContextShift](
|
||||||
config: SchedulerConfig
|
config: SchedulerConfig,
|
||||||
, tasks: JobTaskRegistry[F]
|
tasks: JobTaskRegistry[F],
|
||||||
, store: Store[F]
|
store: Store[F],
|
||||||
, blocker: Blocker
|
blocker: Blocker,
|
||||||
, queue: Resource[F, JobQueue[F]]
|
queue: Resource[F, JobQueue[F]],
|
||||||
, logSink: LogSink[F]) {
|
logSink: LogSink[F]
|
||||||
|
) {
|
||||||
|
|
||||||
def withConfig(cfg: SchedulerConfig): SchedulerBuilder[F] =
|
def withConfig(cfg: SchedulerConfig): SchedulerBuilder[F] =
|
||||||
copy(config = cfg)
|
copy(config = cfg)
|
||||||
@ -33,7 +34,6 @@ case class SchedulerBuilder[F[_]: ConcurrentEffect : ContextShift](
|
|||||||
def withLogSink(sink: LogSink[F]): SchedulerBuilder[F] =
|
def withLogSink(sink: LogSink[F]): SchedulerBuilder[F] =
|
||||||
copy(logSink = sink)
|
copy(logSink = sink)
|
||||||
|
|
||||||
|
|
||||||
def serve: Resource[F, Scheduler[F]] =
|
def serve: Resource[F, Scheduler[F]] =
|
||||||
resource.evalMap(sch => ConcurrentEffect[F].start(sch.start.compile.drain).map(_ => sch))
|
resource.evalMap(sch => ConcurrentEffect[F].start(sch.start.compile.drain).map(_ => sch))
|
||||||
|
|
||||||
@ -45,22 +45,25 @@ case class SchedulerBuilder[F[_]: ConcurrentEffect : ContextShift](
|
|||||||
perms <- Resource.liftF(Semaphore(config.poolSize.toLong))
|
perms <- Resource.liftF(Semaphore(config.poolSize.toLong))
|
||||||
} yield new SchedulerImpl[F](config, blocker, jq, tasks, store, logSink, state, waiter, perms)
|
} yield new SchedulerImpl[F](config, blocker, jq, tasks, store, logSink, state, waiter, perms)
|
||||||
|
|
||||||
scheduler.evalTap(_.init).
|
scheduler.evalTap(_.init).map(s => s: Scheduler[F])
|
||||||
map(s => s: Scheduler[F])
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
object SchedulerBuilder {
|
object SchedulerBuilder {
|
||||||
|
|
||||||
def apply[F[_]: ConcurrentEffect : ContextShift]( config: SchedulerConfig
|
def apply[F[_]: ConcurrentEffect: ContextShift](
|
||||||
, blocker: Blocker
|
config: SchedulerConfig,
|
||||||
, store: Store[F]): SchedulerBuilder[F] =
|
blocker: Blocker,
|
||||||
new SchedulerBuilder[F](config
|
store: Store[F]
|
||||||
, JobTaskRegistry.empty[F]
|
): SchedulerBuilder[F] =
|
||||||
, store
|
new SchedulerBuilder[F](
|
||||||
, blocker
|
config,
|
||||||
, JobQueue(store)
|
JobTaskRegistry.empty[F],
|
||||||
, LogSink.db[F](store))
|
store,
|
||||||
|
blocker,
|
||||||
|
JobQueue(store),
|
||||||
|
LogSink.db[F](store)
|
||||||
|
)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -2,24 +2,26 @@ package docspell.joex.scheduler
|
|||||||
|
|
||||||
import docspell.common._
|
import docspell.common._
|
||||||
|
|
||||||
case class SchedulerConfig( name: Ident
|
case class SchedulerConfig(
|
||||||
, poolSize: Int
|
name: Ident,
|
||||||
, countingScheme: CountingScheme
|
poolSize: Int,
|
||||||
, retries: Int
|
countingScheme: CountingScheme,
|
||||||
, retryDelay: Duration
|
retries: Int,
|
||||||
, logBufferSize: Int
|
retryDelay: Duration,
|
||||||
, wakeupPeriod: Duration
|
logBufferSize: Int,
|
||||||
)
|
wakeupPeriod: Duration
|
||||||
|
)
|
||||||
|
|
||||||
object SchedulerConfig {
|
object SchedulerConfig {
|
||||||
|
|
||||||
val default = SchedulerConfig(
|
val default = SchedulerConfig(
|
||||||
name = Ident.unsafe("default-scheduler")
|
name = Ident.unsafe("default-scheduler"),
|
||||||
, poolSize = 2 // math.max(2, Runtime.getRuntime.availableProcessors / 2)
|
poolSize = 2 // math.max(2, Runtime.getRuntime.availableProcessors / 2)
|
||||||
, countingScheme = CountingScheme(2, 1)
|
,
|
||||||
, retries = 5
|
countingScheme = CountingScheme(2, 1),
|
||||||
, retryDelay = Duration.seconds(30)
|
retries = 5,
|
||||||
, logBufferSize = 500
|
retryDelay = Duration.seconds(30),
|
||||||
, wakeupPeriod = Duration.minutes(10)
|
logBufferSize = 500,
|
||||||
|
wakeupPeriod = Duration.minutes(10)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -14,17 +14,19 @@ import SchedulerImpl._
|
|||||||
import docspell.store.Store
|
import docspell.store.Store
|
||||||
import docspell.store.queries.QJob
|
import docspell.store.queries.QJob
|
||||||
|
|
||||||
final class SchedulerImpl[F[_]: ConcurrentEffect : ContextShift](val config: SchedulerConfig
|
final class SchedulerImpl[F[_]: ConcurrentEffect: ContextShift](
|
||||||
, blocker: Blocker
|
val config: SchedulerConfig,
|
||||||
, queue: JobQueue[F]
|
blocker: Blocker,
|
||||||
, tasks: JobTaskRegistry[F]
|
queue: JobQueue[F],
|
||||||
, store: Store[F]
|
tasks: JobTaskRegistry[F],
|
||||||
, logSink: LogSink[F]
|
store: Store[F],
|
||||||
, state: SignallingRef[F, State[F]]
|
logSink: LogSink[F],
|
||||||
, waiter: SignallingRef[F, Boolean]
|
state: SignallingRef[F, State[F]],
|
||||||
, permits: Semaphore[F]) extends Scheduler[F] {
|
waiter: SignallingRef[F, Boolean],
|
||||||
|
permits: Semaphore[F]
|
||||||
|
) extends Scheduler[F] {
|
||||||
|
|
||||||
private [this] val logger = getLogger
|
private[this] val logger = getLogger
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* On startup, get all jobs in state running from this scheduler
|
* On startup, get all jobs in state running from this scheduler
|
||||||
@ -34,8 +36,13 @@ final class SchedulerImpl[F[_]: ConcurrentEffect : ContextShift](val config: Sch
|
|||||||
QJob.runningToWaiting(config.name, store)
|
QJob.runningToWaiting(config.name, store)
|
||||||
|
|
||||||
def periodicAwake(implicit T: Timer[F]): F[Fiber[F, Unit]] =
|
def periodicAwake(implicit T: Timer[F]): F[Fiber[F, Unit]] =
|
||||||
ConcurrentEffect[F].start(Stream.awakeEvery[F](config.wakeupPeriod.toScala).
|
ConcurrentEffect[F].start(
|
||||||
evalMap(_ => logger.fdebug("Periodic awake reached") *> notifyChange).compile.drain)
|
Stream
|
||||||
|
.awakeEvery[F](config.wakeupPeriod.toScala)
|
||||||
|
.evalMap(_ => logger.fdebug("Periodic awake reached") *> notifyChange)
|
||||||
|
.compile
|
||||||
|
.drain
|
||||||
|
)
|
||||||
|
|
||||||
def getRunning: F[Vector[RJob]] =
|
def getRunning: F[Vector[RJob]] =
|
||||||
state.get.flatMap(s => QJob.findAll(s.getRunning, store))
|
state.get.flatMap(s => QJob.findAll(s.getRunning, store))
|
||||||
@ -51,21 +58,23 @@ final class SchedulerImpl[F[_]: ConcurrentEffect : ContextShift](val config: Sch
|
|||||||
|
|
||||||
def shutdown(cancelAll: Boolean): F[Unit] = {
|
def shutdown(cancelAll: Boolean): F[Unit] = {
|
||||||
val doCancel =
|
val doCancel =
|
||||||
state.get.
|
state.get.flatMap(_.cancelTokens.values.toList.traverse(identity)).map(_ => ())
|
||||||
flatMap(_.cancelTokens.values.toList.traverse(identity)).
|
|
||||||
map(_ => ())
|
|
||||||
|
|
||||||
val runShutdown =
|
val runShutdown =
|
||||||
state.modify(_.requestShutdown) *> (if (cancelAll) doCancel else ().pure[F])
|
state.modify(_.requestShutdown) *> (if (cancelAll) doCancel else ().pure[F])
|
||||||
|
|
||||||
val wait = Stream.eval(runShutdown).
|
val wait = Stream
|
||||||
evalMap(_ => logger.finfo("Scheduler is shutting down now.")).
|
.eval(runShutdown)
|
||||||
flatMap(_ => Stream.eval(state.get) ++ Stream.suspend(state.discrete.takeWhile(_.getRunning.nonEmpty))).
|
.evalMap(_ => logger.finfo("Scheduler is shutting down now."))
|
||||||
flatMap(state => {
|
.flatMap(_ =>
|
||||||
|
Stream.eval(state.get) ++ Stream.suspend(state.discrete.takeWhile(_.getRunning.nonEmpty))
|
||||||
|
)
|
||||||
|
.flatMap { state =>
|
||||||
if (state.getRunning.isEmpty) Stream.eval(logger.finfo("No jobs running."))
|
if (state.getRunning.isEmpty) Stream.eval(logger.finfo("No jobs running."))
|
||||||
else Stream.eval(logger.finfo(s"Waiting for ${state.getRunning.size} jobs to finish.")) ++
|
else
|
||||||
|
Stream.eval(logger.finfo(s"Waiting for ${state.getRunning.size} jobs to finish.")) ++
|
||||||
Stream.emit(state)
|
Stream.emit(state)
|
||||||
})
|
}
|
||||||
|
|
||||||
(wait.drain ++ Stream.emit(())).compile.lastOrError
|
(wait.drain ++ Stream.emit(())).compile.lastOrError
|
||||||
}
|
}
|
||||||
@ -82,15 +91,24 @@ final class SchedulerImpl[F[_]: ConcurrentEffect : ContextShift](val config: Sch
|
|||||||
_ <- logger.fdebug("New permit acquired")
|
_ <- logger.fdebug("New permit acquired")
|
||||||
down <- state.get.map(_.shutdownRequest)
|
down <- state.get.map(_.shutdownRequest)
|
||||||
rjob <- if (down) logger.finfo("") *> permits.release *> (None: Option[RJob]).pure[F]
|
rjob <- if (down) logger.finfo("") *> permits.release *> (None: Option[RJob]).pure[F]
|
||||||
else queue.nextJob(group => state.modify(_.nextPrio(group, config.countingScheme)), config.name, config.retryDelay)
|
else
|
||||||
|
queue.nextJob(
|
||||||
|
group => state.modify(_.nextPrio(group, config.countingScheme)),
|
||||||
|
config.name,
|
||||||
|
config.retryDelay
|
||||||
|
)
|
||||||
_ <- logger.fdebug(s"Next job found: ${rjob.map(_.info)}")
|
_ <- logger.fdebug(s"Next job found: ${rjob.map(_.info)}")
|
||||||
_ <- rjob.map(execute).getOrElse(permits.release)
|
_ <- rjob.map(execute).getOrElse(permits.release)
|
||||||
} yield rjob.isDefined
|
} yield rjob.isDefined
|
||||||
|
|
||||||
Stream.eval(state.get.map(_.shutdownRequest)).
|
Stream
|
||||||
evalTap(if (_) logger.finfo[F]("Stopping main loop due to shutdown request.") else ().pure[F]).
|
.eval(state.get.map(_.shutdownRequest))
|
||||||
flatMap(if (_) Stream.empty else Stream.eval(body)).
|
.evalTap(
|
||||||
flatMap({
|
if (_) logger.finfo[F]("Stopping main loop due to shutdown request.")
|
||||||
|
else ().pure[F]
|
||||||
|
)
|
||||||
|
.flatMap(if (_) Stream.empty else Stream.eval(body))
|
||||||
|
.flatMap({
|
||||||
case true =>
|
case true =>
|
||||||
mainLoop
|
mainLoop
|
||||||
case false =>
|
case false =>
|
||||||
@ -103,7 +121,9 @@ final class SchedulerImpl[F[_]: ConcurrentEffect : ContextShift](val config: Sch
|
|||||||
|
|
||||||
def execute(job: RJob): F[Unit] = {
|
def execute(job: RJob): F[Unit] = {
|
||||||
val task = for {
|
val task = for {
|
||||||
jobtask <- tasks.find(job.task).toRight(s"This executor cannot run tasks with name: ${job.task}")
|
jobtask <- tasks
|
||||||
|
.find(job.task)
|
||||||
|
.toRight(s"This executor cannot run tasks with name: ${job.task}")
|
||||||
} yield jobtask
|
} yield jobtask
|
||||||
|
|
||||||
task match {
|
task match {
|
||||||
@ -123,7 +143,9 @@ final class SchedulerImpl[F[_]: ConcurrentEffect : ContextShift](val config: Sch
|
|||||||
def onFinish(job: RJob, finalState: JobState): F[Unit] =
|
def onFinish(job: RJob, finalState: JobState): F[Unit] =
|
||||||
for {
|
for {
|
||||||
_ <- logger.fdebug(s"Job ${job.info} done $finalState. Releasing resources.")
|
_ <- logger.fdebug(s"Job ${job.info} done $finalState. Releasing resources.")
|
||||||
_ <- permits.release *> permits.available.flatMap(a => logger.fdebug(s"Permit released ($a free)"))
|
_ <- permits.release *> permits.available.flatMap(a =>
|
||||||
|
logger.fdebug(s"Permit released ($a free)")
|
||||||
|
)
|
||||||
_ <- state.modify(_.removeRunning(job))
|
_ <- state.modify(_.removeRunning(job))
|
||||||
_ <- QJob.setFinalState(job.id, finalState, store)
|
_ <- QJob.setFinalState(job.id, finalState, store)
|
||||||
} yield ()
|
} yield ()
|
||||||
@ -131,9 +153,14 @@ final class SchedulerImpl[F[_]: ConcurrentEffect : ContextShift](val config: Sch
|
|||||||
def onStart(job: RJob): F[Unit] =
|
def onStart(job: RJob): F[Unit] =
|
||||||
QJob.setRunning(job.id, config.name, store) //also increments retries if current state=stuck
|
QJob.setRunning(job.id, config.name, store) //also increments retries if current state=stuck
|
||||||
|
|
||||||
def wrapTask(job: RJob, task: Task[F, String, Unit], ctx: Context[F, String]): Task[F, String, Unit] = {
|
def wrapTask(
|
||||||
task.mapF(fa => onStart(job) *> logger.fdebug("Starting task now") *> blocker.blockOn(fa)).
|
job: RJob,
|
||||||
mapF(_.attempt.flatMap({
|
task: Task[F, String, Unit],
|
||||||
|
ctx: Context[F, String]
|
||||||
|
): Task[F, String, Unit] =
|
||||||
|
task
|
||||||
|
.mapF(fa => onStart(job) *> logger.fdebug("Starting task now") *> blocker.blockOn(fa))
|
||||||
|
.mapF(_.attempt.flatMap({
|
||||||
case Right(()) =>
|
case Right(()) =>
|
||||||
logger.info(s"Job execution successful: ${job.info}")
|
logger.info(s"Job execution successful: ${job.info}")
|
||||||
ctx.logger.info("Job execution successful") *>
|
ctx.logger.info("Job execution successful") *>
|
||||||
@ -148,16 +175,18 @@ final class SchedulerImpl[F[_]: ConcurrentEffect : ContextShift](val config: Sch
|
|||||||
QJob.exceedsRetries(job.id, config.retries, store).flatMap {
|
QJob.exceedsRetries(job.id, config.retries, store).flatMap {
|
||||||
case true =>
|
case true =>
|
||||||
logger.error(ex)(s"Job ${job.info} execution failed. Retries exceeded.")
|
logger.error(ex)(s"Job ${job.info} execution failed. Retries exceeded.")
|
||||||
ctx.logger.error(ex)(s"Job ${job.info} execution failed. Retries exceeded.").
|
ctx.logger
|
||||||
map(_ => JobState.Failed: JobState)
|
.error(ex)(s"Job ${job.info} execution failed. Retries exceeded.")
|
||||||
|
.map(_ => JobState.Failed: JobState)
|
||||||
case false =>
|
case false =>
|
||||||
logger.error(ex)(s"Job ${job.info} execution failed. Retrying later.")
|
logger.error(ex)(s"Job ${job.info} execution failed. Retrying later.")
|
||||||
ctx.logger.error(ex)(s"Job ${job.info} execution failed. Retrying later.").
|
ctx.logger
|
||||||
map(_ => JobState.Stuck: JobState)
|
.error(ex)(s"Job ${job.info} execution failed. Retrying later.")
|
||||||
|
.map(_ => JobState.Stuck: JobState)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})).
|
}))
|
||||||
mapF(_.attempt.flatMap {
|
.mapF(_.attempt.flatMap {
|
||||||
case Right(jstate) =>
|
case Right(jstate) =>
|
||||||
onFinish(job, jstate)
|
onFinish(job, jstate)
|
||||||
case Left(ex) =>
|
case Left(ex) =>
|
||||||
@ -166,13 +195,13 @@ final class SchedulerImpl[F[_]: ConcurrentEffect : ContextShift](val config: Sch
|
|||||||
// since tasks should be idempotent, set it to stuck. if above has failed, this might fail anyways
|
// since tasks should be idempotent, set it to stuck. if above has failed, this might fail anyways
|
||||||
onFinish(job, JobState.Stuck)
|
onFinish(job, JobState.Stuck)
|
||||||
})
|
})
|
||||||
}
|
|
||||||
|
|
||||||
def forkRun(job: RJob, code: F[Unit], onCancel: F[Unit], ctx: Context[F, String]): F[F[Unit]] = {
|
def forkRun(job: RJob, code: F[Unit], onCancel: F[Unit], ctx: Context[F, String]): F[F[Unit]] = {
|
||||||
val bfa = blocker.blockOn(code)
|
val bfa = blocker.blockOn(code)
|
||||||
logger.fdebug(s"Forking job ${job.info}") *>
|
logger.fdebug(s"Forking job ${job.info}") *>
|
||||||
ConcurrentEffect[F].start(bfa).
|
ConcurrentEffect[F]
|
||||||
map(fiber =>
|
.start(bfa)
|
||||||
|
.map(fiber =>
|
||||||
logger.fdebug(s"Cancelling job ${job.info}") *>
|
logger.fdebug(s"Cancelling job ${job.info}") *>
|
||||||
fiber.cancel *>
|
fiber.cancel *>
|
||||||
onCancel.attempt.map({
|
onCancel.attempt.map({
|
||||||
@ -184,7 +213,8 @@ final class SchedulerImpl[F[_]: ConcurrentEffect : ContextShift](val config: Sch
|
|||||||
state.modify(_.markCancelled(job)) *>
|
state.modify(_.markCancelled(job)) *>
|
||||||
onFinish(job, JobState.Cancelled) *>
|
onFinish(job, JobState.Cancelled) *>
|
||||||
ctx.logger.warn("Job has been cancelled.") *>
|
ctx.logger.warn("Job has been cancelled.") *>
|
||||||
logger.fdebug(s"Job ${job.info} has been cancelled."))
|
logger.fdebug(s"Job ${job.info} has been cancelled.")
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -193,10 +223,12 @@ object SchedulerImpl {
|
|||||||
def emptyState[F[_]]: State[F] =
|
def emptyState[F[_]]: State[F] =
|
||||||
State(Map.empty, Set.empty, Map.empty, false)
|
State(Map.empty, Set.empty, Map.empty, false)
|
||||||
|
|
||||||
case class State[F[_]]( counters: Map[Ident, CountingScheme]
|
case class State[F[_]](
|
||||||
, cancelled: Set[Ident]
|
counters: Map[Ident, CountingScheme],
|
||||||
, cancelTokens: Map[Ident, CancelToken[F]]
|
cancelled: Set[Ident],
|
||||||
, shutdownRequest: Boolean) {
|
cancelTokens: Map[Ident, CancelToken[F]],
|
||||||
|
shutdownRequest: Boolean
|
||||||
|
) {
|
||||||
|
|
||||||
def nextPrio(group: Ident, initial: CountingScheme): (State[F], Priority) = {
|
def nextPrio(group: Ident, initial: CountingScheme): (State[F], Priority) = {
|
||||||
val (cs, prio) = counters.getOrElse(group, initial).nextPriority
|
val (cs, prio) = counters.getOrElse(group, initial).nextPriority
|
||||||
|
@ -24,11 +24,11 @@ trait Task[F[_], A, B] {
|
|||||||
def mapF[C](f: F[B] => F[C]): Task[F, A, C] =
|
def mapF[C](f: F[B] => F[C]): Task[F, A, C] =
|
||||||
Task(Task.toKleisli(this).mapF(f))
|
Task(Task.toKleisli(this).mapF(f))
|
||||||
|
|
||||||
def attempt(implicit F: ApplicativeError[F,Throwable]): Task[F, A, Either[Throwable, B]] =
|
def attempt(implicit F: ApplicativeError[F, Throwable]): Task[F, A, Either[Throwable, B]] =
|
||||||
mapF(_.attempt)
|
mapF(_.attempt)
|
||||||
|
|
||||||
def contramap[C](f: C => F[A])(implicit F: FlatMap[F]): Task[F, C, B] = {
|
def contramap[C](f: C => F[A])(implicit F: FlatMap[F]): Task[F, C, B] = { ctxc: Context[F, C] =>
|
||||||
ctxc: Context[F, C] => f(ctxc.args).flatMap(a => run(ctxc.map(_ => a)))
|
f(ctxc.args).flatMap(a => run(ctxc.map(_ => a)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -46,7 +46,6 @@ object Task {
|
|||||||
def apply[F[_], A, B](k: Kleisli[F, Context[F, A], B]): Task[F, A, B] =
|
def apply[F[_], A, B](k: Kleisli[F, Context[F, A], B]): Task[F, A, B] =
|
||||||
c => k.run(c)
|
c => k.run(c)
|
||||||
|
|
||||||
|
|
||||||
def toKleisli[F[_], A, B](t: Task[F, A, B]): Kleisli[F, Context[F, A], B] =
|
def toKleisli[F[_], A, B](t: Task[F, A, B]): Kleisli[F, Context[F, A], B] =
|
||||||
Kleisli(t.run)
|
Kleisli(t.run)
|
||||||
|
|
||||||
|
@ -6,7 +6,7 @@ import minitest.SimpleTestSuite
|
|||||||
object CountingSchemeSpec extends SimpleTestSuite {
|
object CountingSchemeSpec extends SimpleTestSuite {
|
||||||
|
|
||||||
test("counting") {
|
test("counting") {
|
||||||
val cs = CountingScheme(2,1)
|
val cs = CountingScheme(2, 1)
|
||||||
val list = List.iterate(cs.nextPriority, 6)(_._1.nextPriority).map(_._2)
|
val list = List.iterate(cs.nextPriority, 6)(_._1.nextPriority).map(_._2)
|
||||||
val expect = List(Priority.High, Priority.High, Priority.Low)
|
val expect = List(Priority.High, Priority.High, Priority.Low)
|
||||||
assertEquals(list, expect ++ expect)
|
assertEquals(list, expect ++ expect)
|
||||||
|
@ -7,27 +7,37 @@ import docspell.backend.{Config => BackendConfig}
|
|||||||
import docspell.common._
|
import docspell.common._
|
||||||
import scodec.bits.ByteVector
|
import scodec.bits.ByteVector
|
||||||
|
|
||||||
case class Config(appName: String
|
case class Config(
|
||||||
, appId: Ident
|
appName: String,
|
||||||
, baseUrl: LenientUri
|
appId: Ident,
|
||||||
, bind: Config.Bind
|
baseUrl: LenientUri,
|
||||||
, backend: BackendConfig
|
bind: Config.Bind,
|
||||||
, auth: Login.Config
|
backend: BackendConfig,
|
||||||
|
auth: Login.Config
|
||||||
)
|
)
|
||||||
|
|
||||||
object Config {
|
object Config {
|
||||||
val postgres = JdbcConfig(LenientUri.unsafe("jdbc:postgresql://localhost:5432/docspelldev"), "dev", "dev")
|
val postgres =
|
||||||
val h2 = JdbcConfig(LenientUri.unsafe("jdbc:h2:./target/docspelldev.db;MODE=PostgreSQL;DATABASE_TO_LOWER=TRUE"), "sa", "")
|
JdbcConfig(LenientUri.unsafe("jdbc:postgresql://localhost:5432/docspelldev"), "dev", "dev")
|
||||||
|
val h2 = JdbcConfig(
|
||||||
|
LenientUri.unsafe("jdbc:h2:./target/docspelldev.db;MODE=PostgreSQL;DATABASE_TO_LOWER=TRUE"),
|
||||||
|
"sa",
|
||||||
|
""
|
||||||
|
)
|
||||||
|
|
||||||
val default: Config =
|
val default: Config =
|
||||||
Config("Docspell"
|
Config(
|
||||||
, Ident.unsafe("restserver1")
|
"Docspell",
|
||||||
, LenientUri.unsafe("http://localhost:7880")
|
Ident.unsafe("restserver1"),
|
||||||
, Config.Bind("localhost", 7880)
|
LenientUri.unsafe("http://localhost:7880"),
|
||||||
, BackendConfig(postgres
|
Config.Bind("localhost", 7880),
|
||||||
, SignupConfig(SignupConfig.invite, Password("testpass"), Duration.hours(5 * 24))
|
BackendConfig(
|
||||||
, BackendConfig.Files(512 * 1024, List(MimeType.pdf)))
|
postgres,
|
||||||
, Login.Config(ByteVector.fromValidHex("caffee"), Duration.minutes(2)))
|
SignupConfig(SignupConfig.invite, Password("testpass"), Duration.hours(5 * 24)),
|
||||||
|
BackendConfig.Files(512 * 1024, List(MimeType.pdf))
|
||||||
|
),
|
||||||
|
Login.Config(ByteVector.fromValidHex("caffee"), Duration.minutes(2))
|
||||||
|
)
|
||||||
|
|
||||||
case class Bind(address: String, port: Int)
|
case class Bind(address: String, port: Int)
|
||||||
}
|
}
|
||||||
|
@ -13,12 +13,14 @@ import org.log4s._
|
|||||||
object Main extends IOApp {
|
object Main extends IOApp {
|
||||||
private[this] val logger = getLogger
|
private[this] val logger = getLogger
|
||||||
|
|
||||||
val blockingEc: ExecutionContext = ExecutionContext.fromExecutor(Executors.newCachedThreadPool(
|
val blockingEc: ExecutionContext = ExecutionContext.fromExecutor(
|
||||||
ThreadFactories.ofName("docspell-restserver-blocking")))
|
Executors.newCachedThreadPool(ThreadFactories.ofName("docspell-restserver-blocking"))
|
||||||
|
)
|
||||||
val blocker = Blocker.liftExecutionContext(blockingEc)
|
val blocker = Blocker.liftExecutionContext(blockingEc)
|
||||||
|
|
||||||
val connectEC: ExecutionContext = ExecutionContext.fromExecutorService(Executors.newFixedThreadPool(5,
|
val connectEC: ExecutionContext = ExecutionContext.fromExecutorService(
|
||||||
ThreadFactories.ofName("docspell-dbconnect")))
|
Executors.newFixedThreadPool(5, ThreadFactories.ofName("docspell-dbconnect"))
|
||||||
|
)
|
||||||
|
|
||||||
def run(args: List[String]) = {
|
def run(args: List[String]) = {
|
||||||
args match {
|
args match {
|
||||||
@ -41,12 +43,15 @@ object Main extends IOApp {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val cfg = ConfigFile.loadConfig
|
val cfg = ConfigFile.loadConfig
|
||||||
val banner = Banner("REST Server"
|
val banner = Banner(
|
||||||
, BuildInfo.version
|
"REST Server",
|
||||||
, BuildInfo.gitHeadCommit
|
BuildInfo.version,
|
||||||
, cfg.backend.jdbc.url
|
BuildInfo.gitHeadCommit,
|
||||||
, Option(System.getProperty("config.file"))
|
cfg.backend.jdbc.url,
|
||||||
, cfg.appId, cfg.baseUrl)
|
Option(System.getProperty("config.file")),
|
||||||
|
cfg.appId,
|
||||||
|
cfg.baseUrl
|
||||||
|
)
|
||||||
logger.info(s"\n${banner.render("***>")}")
|
logger.info(s"\n${banner.render("***>")}")
|
||||||
RestServer.stream[IO](cfg, connectEC, blockingEc, blocker).compile.drain.as(ExitCode.Success)
|
RestServer.stream[IO](cfg, connectEC, blockingEc, blocker).compile.drain.as(ExitCode.Success)
|
||||||
}
|
}
|
||||||
|
@ -7,7 +7,8 @@ import docspell.common.NodeType
|
|||||||
|
|
||||||
import scala.concurrent.ExecutionContext
|
import scala.concurrent.ExecutionContext
|
||||||
|
|
||||||
final class RestAppImpl[F[_]: Sync](val config: Config, val backend: BackendApp[F]) extends RestApp[F] {
|
final class RestAppImpl[F[_]: Sync](val config: Config, val backend: BackendApp[F])
|
||||||
|
extends RestApp[F] {
|
||||||
|
|
||||||
def init: F[Unit] =
|
def init: F[Unit] =
|
||||||
backend.node.register(config.appId, NodeType.Restserver, config.baseUrl)
|
backend.node.register(config.appId, NodeType.Restserver, config.baseUrl)
|
||||||
@ -18,7 +19,12 @@ final class RestAppImpl[F[_]: Sync](val config: Config, val backend: BackendApp[
|
|||||||
|
|
||||||
object RestAppImpl {
|
object RestAppImpl {
|
||||||
|
|
||||||
def create[F[_]: ConcurrentEffect: ContextShift](cfg: Config, connectEC: ExecutionContext, httpClientEc: ExecutionContext, blocker: Blocker): Resource[F, RestApp[F]] =
|
def create[F[_]: ConcurrentEffect: ContextShift](
|
||||||
|
cfg: Config,
|
||||||
|
connectEC: ExecutionContext,
|
||||||
|
httpClientEc: ExecutionContext,
|
||||||
|
blocker: Blocker
|
||||||
|
): Resource[F, RestApp[F]] =
|
||||||
for {
|
for {
|
||||||
backend <- BackendApp(cfg.backend, connectEC, httpClientEc, blocker)
|
backend <- BackendApp(cfg.backend, connectEC, httpClientEc, blocker)
|
||||||
app = new RestAppImpl[F](cfg, backend)
|
app = new RestAppImpl[F](cfg, backend)
|
||||||
|
@ -15,8 +15,12 @@ import scala.concurrent.ExecutionContext
|
|||||||
|
|
||||||
object RestServer {
|
object RestServer {
|
||||||
|
|
||||||
def stream[F[_]: ConcurrentEffect](cfg: Config, connectEC: ExecutionContext, httpClientEc: ExecutionContext, blocker: Blocker)
|
def stream[F[_]: ConcurrentEffect](
|
||||||
(implicit T: Timer[F], CS: ContextShift[F]): Stream[F, Nothing] = {
|
cfg: Config,
|
||||||
|
connectEC: ExecutionContext,
|
||||||
|
httpClientEc: ExecutionContext,
|
||||||
|
blocker: Blocker
|
||||||
|
)(implicit T: Timer[F], CS: ContextShift[F]): Stream[F, Nothing] = {
|
||||||
|
|
||||||
val app = for {
|
val app = for {
|
||||||
restApp <- RestAppImpl.create[F](cfg, connectEC, httpClientEc, blocker)
|
restApp <- RestAppImpl.create[F](cfg, connectEC, httpClientEc, blocker)
|
||||||
@ -24,8 +28,8 @@ object RestServer {
|
|||||||
httpApp = Router(
|
httpApp = Router(
|
||||||
"/api/info" -> routes.InfoRoutes(),
|
"/api/info" -> routes.InfoRoutes(),
|
||||||
"/api/v1/open/" -> openRoutes(cfg, restApp),
|
"/api/v1/open/" -> openRoutes(cfg, restApp),
|
||||||
"/api/v1/sec/" -> Authenticate(restApp.backend.login, cfg.auth) {
|
"/api/v1/sec/" -> Authenticate(restApp.backend.login, cfg.auth) { token =>
|
||||||
token => securedRoutes(cfg, restApp, token)
|
securedRoutes(cfg, restApp, token)
|
||||||
},
|
},
|
||||||
"/app/assets" -> WebjarRoutes.appRoutes[F](blocker),
|
"/app/assets" -> WebjarRoutes.appRoutes[F](blocker),
|
||||||
"/app" -> TemplateRoutes[F](blocker, cfg)
|
"/app" -> TemplateRoutes[F](blocker, cfg)
|
||||||
@ -35,16 +39,22 @@ object RestServer {
|
|||||||
|
|
||||||
} yield finalHttpApp
|
} yield finalHttpApp
|
||||||
|
|
||||||
Stream.resource(app).flatMap(httpApp =>
|
Stream
|
||||||
BlazeServerBuilder[F].
|
.resource(app)
|
||||||
bindHttp(cfg.bind.port, cfg.bind.address).
|
.flatMap(httpApp =>
|
||||||
withHttpApp(httpApp).
|
BlazeServerBuilder[F]
|
||||||
withoutBanner.
|
.bindHttp(cfg.bind.port, cfg.bind.address)
|
||||||
serve)
|
.withHttpApp(httpApp)
|
||||||
|
.withoutBanner
|
||||||
|
.serve
|
||||||
|
)
|
||||||
}.drain
|
}.drain
|
||||||
|
|
||||||
|
def securedRoutes[F[_]: Effect](
|
||||||
def securedRoutes[F[_]: Effect](cfg: Config, restApp: RestApp[F], token: AuthToken): HttpRoutes[F] =
|
cfg: Config,
|
||||||
|
restApp: RestApp[F],
|
||||||
|
token: AuthToken
|
||||||
|
): 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),
|
||||||
|
@ -13,8 +13,15 @@ case class CookieData(auth: AuthToken) {
|
|||||||
def asCookie(cfg: Config): ResponseCookie = {
|
def asCookie(cfg: Config): ResponseCookie = {
|
||||||
val domain = cfg.baseUrl.host
|
val domain = cfg.baseUrl.host
|
||||||
val sec = cfg.baseUrl.scheme.exists(_.endsWith("s"))
|
val sec = cfg.baseUrl.scheme.exists(_.endsWith("s"))
|
||||||
val path = cfg.baseUrl.path/"api"/"v1"/"sec"
|
val path = cfg.baseUrl.path / "api" / "v1" / "sec"
|
||||||
ResponseCookie(CookieData.cookieName, asString, domain = domain, path = Some(path.asString), httpOnly = true, secure = sec)
|
ResponseCookie(
|
||||||
|
CookieData.cookieName,
|
||||||
|
asString,
|
||||||
|
domain = domain,
|
||||||
|
path = Some(path.asString),
|
||||||
|
httpOnly = true,
|
||||||
|
secure = sec
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
object CookieData {
|
object CookieData {
|
||||||
@ -22,18 +29,21 @@ object CookieData {
|
|||||||
val headerName = "X-Docspell-Auth"
|
val headerName = "X-Docspell-Auth"
|
||||||
|
|
||||||
def authenticator[F[_]](r: Request[F]): Either[String, String] =
|
def authenticator[F[_]](r: Request[F]): Either[String, String] =
|
||||||
fromCookie(r) orElse fromHeader(r)
|
fromCookie(r).orElse(fromHeader(r))
|
||||||
|
|
||||||
def fromCookie[F[_]](req: Request[F]): Either[String, String] = {
|
def fromCookie[F[_]](req: Request[F]): Either[String, String] =
|
||||||
for {
|
for {
|
||||||
header <- headers.Cookie.from(req.headers).toRight("Cookie parsing error")
|
header <- headers.Cookie.from(req.headers).toRight("Cookie parsing error")
|
||||||
cookie <- header.values.toList.find(_.name == cookieName).toRight("Couldn't find the authcookie")
|
cookie <- header.values.toList
|
||||||
|
.find(_.name == cookieName)
|
||||||
|
.toRight("Couldn't find the authcookie")
|
||||||
} yield cookie.content
|
} yield cookie.content
|
||||||
}
|
|
||||||
|
|
||||||
def fromHeader[F[_]](req: Request[F]): Either[String, String] = {
|
def fromHeader[F[_]](req: Request[F]): Either[String, String] =
|
||||||
req.headers.get(CaseInsensitiveString(headerName)).map(_.value).toRight("Couldn't find an authenticator")
|
req.headers
|
||||||
}
|
.get(CaseInsensitiveString(headerName))
|
||||||
|
.map(_.value)
|
||||||
|
.toRight("Couldn't find an authenticator")
|
||||||
|
|
||||||
def deleteCookie(cfg: Config): ResponseCookie =
|
def deleteCookie(cfg: Config): ResponseCookie =
|
||||||
ResponseCookie(
|
ResponseCookie(
|
||||||
|
@ -24,31 +24,37 @@ trait Conversions {
|
|||||||
|
|
||||||
// insights
|
// insights
|
||||||
def mkItemInsights(d: InsightData): ItemInsights =
|
def mkItemInsights(d: InsightData): ItemInsights =
|
||||||
ItemInsights(d.incoming, d.outgoing, d.bytes, TagCloud(d.tags.toList.map(p => NameCount(p._1, p._2))))
|
ItemInsights(
|
||||||
|
d.incoming,
|
||||||
|
d.outgoing,
|
||||||
|
d.bytes,
|
||||||
|
TagCloud(d.tags.toList.map(p => NameCount(p._1, p._2)))
|
||||||
|
)
|
||||||
|
|
||||||
// attachment meta
|
// attachment meta
|
||||||
def mkAttachmentMeta(rm: RAttachmentMeta): AttachmentMeta =
|
def mkAttachmentMeta(rm: RAttachmentMeta): AttachmentMeta =
|
||||||
AttachmentMeta(rm.content.getOrElse("")
|
AttachmentMeta(
|
||||||
, rm.nerlabels.map(nl => Label(nl.tag, nl.label, nl.startPosition, nl.endPosition))
|
rm.content.getOrElse(""),
|
||||||
, mkItemProposals(rm.proposals))
|
rm.nerlabels.map(nl => Label(nl.tag, nl.label, nl.startPosition, nl.endPosition)),
|
||||||
|
mkItemProposals(rm.proposals)
|
||||||
|
)
|
||||||
|
|
||||||
// item proposal
|
// item proposal
|
||||||
def mkItemProposals(ml: MetaProposalList): ItemProposals = {
|
def mkItemProposals(ml: MetaProposalList): ItemProposals = {
|
||||||
def get(mpt: MetaProposalType) =
|
def get(mpt: MetaProposalType) =
|
||||||
ml.find(mpt).
|
ml.find(mpt).map(mp => mp.values.toList.map(_.ref).map(mkIdName)).getOrElse(Nil)
|
||||||
map(mp => mp.values.toList.map(_.ref).map(mkIdName)).
|
|
||||||
getOrElse(Nil)
|
|
||||||
def getDates(mpt: MetaProposalType): List[Timestamp] =
|
def getDates(mpt: MetaProposalType): List[Timestamp] =
|
||||||
ml.find(mpt).
|
ml.find(mpt)
|
||||||
map(mp => mp.values.toList.
|
.map(mp =>
|
||||||
map(cand => cand.ref.id.id).
|
mp.values.toList
|
||||||
flatMap(str => Either.catchNonFatal(LocalDate.parse(str)).toOption).
|
.map(cand => cand.ref.id.id)
|
||||||
map(_.atTime(12, 0).atZone(ZoneId.of("GMT"))).
|
.flatMap(str => Either.catchNonFatal(LocalDate.parse(str)).toOption)
|
||||||
map(zdt => Timestamp(zdt.toInstant))).
|
.map(_.atTime(12, 0).atZone(ZoneId.of("GMT")))
|
||||||
getOrElse(Nil).
|
.map(zdt => Timestamp(zdt.toInstant))
|
||||||
distinct.
|
)
|
||||||
take(5)
|
.getOrElse(Nil)
|
||||||
|
.distinct
|
||||||
|
.take(5)
|
||||||
|
|
||||||
ItemProposals(
|
ItemProposals(
|
||||||
corrOrg = get(MetaProposalType.CorrOrg),
|
corrOrg = get(MetaProposalType.CorrOrg),
|
||||||
@ -62,23 +68,25 @@ trait Conversions {
|
|||||||
|
|
||||||
// item detail
|
// item detail
|
||||||
def mkItemDetail(data: OItem.ItemData): ItemDetail =
|
def mkItemDetail(data: OItem.ItemData): ItemDetail =
|
||||||
ItemDetail(data.item.id
|
ItemDetail(
|
||||||
, data.item.direction
|
data.item.id,
|
||||||
, data.item.name
|
data.item.direction,
|
||||||
, data.item.source
|
data.item.name,
|
||||||
, data.item.state
|
data.item.source,
|
||||||
, data.item.created
|
data.item.state,
|
||||||
, data.item.updated
|
data.item.created,
|
||||||
, data.item.itemDate
|
data.item.updated,
|
||||||
, data.corrOrg.map(o => IdName(o.oid, o.name))
|
data.item.itemDate,
|
||||||
, data.corrPerson.map(p => IdName(p.pid, p.name))
|
data.corrOrg.map(o => IdName(o.oid, o.name)),
|
||||||
, data.concPerson.map(p => IdName(p.pid, p.name))
|
data.corrPerson.map(p => IdName(p.pid, p.name)),
|
||||||
, data.concEquip.map(e => IdName(e.eid, e.name))
|
data.concPerson.map(p => IdName(p.pid, p.name)),
|
||||||
, data.inReplyTo.map(mkIdName)
|
data.concEquip.map(e => IdName(e.eid, e.name)),
|
||||||
, data.item.dueDate
|
data.inReplyTo.map(mkIdName),
|
||||||
, data.item.notes
|
data.item.dueDate,
|
||||||
, data.attachments.map((mkAttachment _).tupled).toList
|
data.item.notes,
|
||||||
, data.tags.map(mkTag).toList)
|
data.attachments.map((mkAttachment _).tupled).toList,
|
||||||
|
data.tags.map(mkTag).toList
|
||||||
|
)
|
||||||
|
|
||||||
def mkAttachment(ra: RAttachment, m: FileMeta): Attachment =
|
def mkAttachment(ra: RAttachment, m: FileMeta): Attachment =
|
||||||
Attachment(ra.id, ra.name, m.length, MimeType.unsafe(m.mimetype.asString))
|
Attachment(ra.id, ra.name, m.length, MimeType.unsafe(m.mimetype.asString))
|
||||||
@ -86,20 +94,21 @@ trait Conversions {
|
|||||||
// item list
|
// item list
|
||||||
|
|
||||||
def mkQuery(m: ItemSearch, coll: Ident): OItem.Query =
|
def mkQuery(m: ItemSearch, coll: Ident): OItem.Query =
|
||||||
OItem.Query(coll
|
OItem.Query(
|
||||||
, m.name
|
coll,
|
||||||
, if (m.inbox) Seq(ItemState.Created) else Seq(ItemState.Created, ItemState.Confirmed)
|
m.name,
|
||||||
, m.direction
|
if (m.inbox) Seq(ItemState.Created) else Seq(ItemState.Created, ItemState.Confirmed),
|
||||||
, m.corrPerson
|
m.direction,
|
||||||
, m.corrOrg
|
m.corrPerson,
|
||||||
, m.concPerson
|
m.corrOrg,
|
||||||
, m.concEquip
|
m.concPerson,
|
||||||
, m.tagsInclude.map(Ident.unsafe)
|
m.concEquip,
|
||||||
, m.tagsExclude.map(Ident.unsafe)
|
m.tagsInclude.map(Ident.unsafe),
|
||||||
, m.dateFrom
|
m.tagsExclude.map(Ident.unsafe),
|
||||||
, m.dateUntil
|
m.dateFrom,
|
||||||
, m.dueDateFrom
|
m.dateUntil,
|
||||||
, m.dueDateUntil
|
m.dueDateFrom,
|
||||||
|
m.dueDateUntil
|
||||||
)
|
)
|
||||||
|
|
||||||
def mkItemList(v: Vector[OItem.ListItem]): ItemLightList = {
|
def mkItemList(v: Vector[OItem.ListItem]): ItemLightList = {
|
||||||
@ -113,8 +122,20 @@ trait Conversions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
def mkItemLight(i: OItem.ListItem): ItemLight =
|
def mkItemLight(i: OItem.ListItem): ItemLight =
|
||||||
ItemLight(i.id, i.name, i.state, i.date, i.dueDate, i.source, i.direction.name.some, i.corrOrg.map(mkIdName),
|
ItemLight(
|
||||||
i.corrPerson.map(mkIdName), i.concPerson.map(mkIdName), i.concEquip.map(mkIdName), i.fileCount)
|
i.id,
|
||||||
|
i.name,
|
||||||
|
i.state,
|
||||||
|
i.date,
|
||||||
|
i.dueDate,
|
||||||
|
i.source,
|
||||||
|
i.direction.name.some,
|
||||||
|
i.corrOrg.map(mkIdName),
|
||||||
|
i.corrPerson.map(mkIdName),
|
||||||
|
i.concPerson.map(mkIdName),
|
||||||
|
i.concEquip.map(mkIdName),
|
||||||
|
i.fileCount
|
||||||
|
)
|
||||||
|
|
||||||
// job
|
// job
|
||||||
def mkJobQueueState(state: OJob.CollectiveQueueState): JobQueueState = {
|
def mkJobQueueState(state: OJob.CollectiveQueueState): JobQueueState = {
|
||||||
@ -128,46 +149,57 @@ trait Conversions {
|
|||||||
val t2 = f(j2).getOrElse(Timestamp.Epoch)
|
val t2 = f(j2).getOrElse(Timestamp.Epoch)
|
||||||
t1.value.isBefore(t2.value)
|
t1.value.isBefore(t2.value)
|
||||||
}
|
}
|
||||||
JobQueueState(state.running.map(mkJobDetail).toList.sortWith(asc(_.started))
|
JobQueueState(
|
||||||
, state.done.map(mkJobDetail).toList.sortWith(desc(_.finished))
|
state.running.map(mkJobDetail).toList.sortWith(asc(_.started)),
|
||||||
, state.queued.map(mkJobDetail).toList.sortWith(asc(_.submitted.some)))
|
state.done.map(mkJobDetail).toList.sortWith(desc(_.finished)),
|
||||||
|
state.queued.map(mkJobDetail).toList.sortWith(asc(_.submitted.some))
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
def mkJobDetail(jd: OJob.JobDetail): JobDetail =
|
def mkJobDetail(jd: OJob.JobDetail): JobDetail =
|
||||||
JobDetail(jd.job.id
|
JobDetail(
|
||||||
, jd.job.subject
|
jd.job.id,
|
||||||
, jd.job.submitted
|
jd.job.subject,
|
||||||
, jd.job.priority
|
jd.job.submitted,
|
||||||
, jd.job.state
|
jd.job.priority,
|
||||||
, jd.job.retries
|
jd.job.state,
|
||||||
, jd.logs.map(mkJobLog).toList
|
jd.job.retries,
|
||||||
, jd.job.progress
|
jd.logs.map(mkJobLog).toList,
|
||||||
, jd.job.worker
|
jd.job.progress,
|
||||||
, jd.job.started
|
jd.job.worker,
|
||||||
, jd.job.finished)
|
jd.job.started,
|
||||||
|
jd.job.finished
|
||||||
|
)
|
||||||
|
|
||||||
def mkJobLog(jl: RJobLog): JobLogEvent =
|
def mkJobLog(jl: RJobLog): JobLogEvent =
|
||||||
JobLogEvent(jl.created, jl.level, jl.message)
|
JobLogEvent(jl.created, jl.level, jl.message)
|
||||||
|
|
||||||
// upload
|
// upload
|
||||||
def readMultipart[F[_]: Effect](mp: Multipart[F], logger: Logger, prio: Priority, validFileTypes: Seq[MimeType]): F[UploadData[F]] = {
|
def readMultipart[F[_]: Effect](
|
||||||
def parseMeta(body: Stream[F, Byte]): F[ItemUploadMeta] = {
|
mp: Multipart[F],
|
||||||
body.through(fs2.text.utf8Decode).
|
logger: Logger,
|
||||||
parseJsonAs[ItemUploadMeta].
|
prio: Priority,
|
||||||
map(_.fold(ex => {
|
validFileTypes: Seq[MimeType]
|
||||||
|
): F[UploadData[F]] = {
|
||||||
|
def parseMeta(body: Stream[F, Byte]): F[ItemUploadMeta] =
|
||||||
|
body
|
||||||
|
.through(fs2.text.utf8Decode)
|
||||||
|
.parseJsonAs[ItemUploadMeta]
|
||||||
|
.map(_.fold(ex => {
|
||||||
logger.error(ex)("Reading upload metadata failed.")
|
logger.error(ex)("Reading upload metadata failed.")
|
||||||
throw ex
|
throw ex
|
||||||
}, identity))
|
}, identity))
|
||||||
}
|
|
||||||
|
|
||||||
val meta: F[(Boolean, UploadMeta)] = mp.parts.find(_.name.exists(_ equalsIgnoreCase "meta")).
|
val meta: F[(Boolean, UploadMeta)] = mp.parts
|
||||||
map(p => parseMeta(p.body)).
|
.find(_.name.exists(_.equalsIgnoreCase("meta")))
|
||||||
map(fm => fm.map(m => (m.multiple, UploadMeta(m.direction, "webapp", validFileTypes)))).
|
.map(p => parseMeta(p.body))
|
||||||
getOrElse((true, UploadMeta(None, "webapp", validFileTypes)).pure[F])
|
.map(fm => fm.map(m => (m.multiple, UploadMeta(m.direction, "webapp", validFileTypes))))
|
||||||
|
.getOrElse((true, UploadMeta(None, "webapp", validFileTypes)).pure[F])
|
||||||
|
|
||||||
val files = mp.parts.
|
val files = mp.parts
|
||||||
filter(p => p.name.forall(s => !s.equalsIgnoreCase("meta"))).
|
.filter(p => p.name.forall(s => !s.equalsIgnoreCase("meta")))
|
||||||
map(p => OUpload.File(p.filename, p.headers.get(`Content-Type`).map(fromContentType), p.body))
|
.map(p => OUpload.File(p.filename, p.headers.get(`Content-Type`).map(fromContentType), p.body)
|
||||||
|
)
|
||||||
for {
|
for {
|
||||||
metaData <- meta
|
metaData <- meta
|
||||||
_ <- Effect[F].delay(logger.debug(s"Parsed upload meta data: $metaData"))
|
_ <- Effect[F].delay(logger.debug(s"Parsed upload meta data: $metaData"))
|
||||||
@ -178,8 +210,14 @@ trait Conversions {
|
|||||||
// organization and person
|
// organization and person
|
||||||
def mkOrg(v: OOrganization.OrgAndContacts): Organization = {
|
def mkOrg(v: OOrganization.OrgAndContacts): Organization = {
|
||||||
val ro = v.org
|
val ro = v.org
|
||||||
Organization(ro.oid, ro.name, Address(ro.street, ro.zip, ro.city, ro.country),
|
Organization(
|
||||||
v.contacts.map(mkContact).toList, ro.notes, ro.created)
|
ro.oid,
|
||||||
|
ro.name,
|
||||||
|
Address(ro.street, ro.zip, ro.city, ro.country),
|
||||||
|
v.contacts.map(mkContact).toList,
|
||||||
|
ro.notes,
|
||||||
|
ro.created
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
def newOrg[F[_]: Sync](v: Organization, cid: Ident): F[OOrganization.OrgAndContacts] = {
|
def newOrg[F[_]: Sync](v: Organization, cid: Ident): F[OOrganization.OrgAndContacts] = {
|
||||||
@ -189,7 +227,17 @@ trait Conversions {
|
|||||||
now <- Timestamp.current[F]
|
now <- Timestamp.current[F]
|
||||||
oid <- Ident.randomId[F]
|
oid <- Ident.randomId[F]
|
||||||
cont <- contacts(oid)
|
cont <- contacts(oid)
|
||||||
org = ROrganization(oid, cid, v.name, v.address.street, v.address.zip, v.address.city, v.address.country, v.notes, now)
|
org = ROrganization(
|
||||||
|
oid,
|
||||||
|
cid,
|
||||||
|
v.name,
|
||||||
|
v.address.street,
|
||||||
|
v.address.zip,
|
||||||
|
v.address.city,
|
||||||
|
v.address.country,
|
||||||
|
v.notes,
|
||||||
|
now
|
||||||
|
)
|
||||||
} yield OOrganization.OrgAndContacts(org, cont)
|
} yield OOrganization.OrgAndContacts(org, cont)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -198,14 +246,31 @@ trait Conversions {
|
|||||||
v.contacts.traverse(c => newContact(c, oid.some, None))
|
v.contacts.traverse(c => newContact(c, oid.some, None))
|
||||||
for {
|
for {
|
||||||
cont <- contacts(v.id)
|
cont <- contacts(v.id)
|
||||||
org = ROrganization(v.id, cid, v.name, v.address.street, v.address.zip, v.address.city, v.address.country, v.notes, v.created)
|
org = ROrganization(
|
||||||
|
v.id,
|
||||||
|
cid,
|
||||||
|
v.name,
|
||||||
|
v.address.street,
|
||||||
|
v.address.zip,
|
||||||
|
v.address.city,
|
||||||
|
v.address.country,
|
||||||
|
v.notes,
|
||||||
|
v.created
|
||||||
|
)
|
||||||
} yield OOrganization.OrgAndContacts(org, cont)
|
} yield OOrganization.OrgAndContacts(org, cont)
|
||||||
}
|
}
|
||||||
|
|
||||||
def mkPerson(v: OOrganization.PersonAndContacts): Person = {
|
def mkPerson(v: OOrganization.PersonAndContacts): Person = {
|
||||||
val ro = v.person
|
val ro = v.person
|
||||||
Person(ro.pid, ro.name, Address(ro.street, ro.zip, ro.city, ro.country),
|
Person(
|
||||||
v.contacts.map(mkContact).toList, ro.notes, ro.concerning, ro.created)
|
ro.pid,
|
||||||
|
ro.name,
|
||||||
|
Address(ro.street, ro.zip, ro.city, ro.country),
|
||||||
|
v.contacts.map(mkContact).toList,
|
||||||
|
ro.notes,
|
||||||
|
ro.concerning,
|
||||||
|
ro.created
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
def newPerson[F[_]: Sync](v: Person, cid: Ident): F[OOrganization.PersonAndContacts] = {
|
def newPerson[F[_]: Sync](v: Person, cid: Ident): F[OOrganization.PersonAndContacts] = {
|
||||||
@ -215,7 +280,18 @@ trait Conversions {
|
|||||||
now <- Timestamp.current[F]
|
now <- Timestamp.current[F]
|
||||||
pid <- Ident.randomId[F]
|
pid <- Ident.randomId[F]
|
||||||
cont <- contacts(pid)
|
cont <- contacts(pid)
|
||||||
org = RPerson(pid, cid, v.name, v.address.street, v.address.zip, v.address.city, v.address.country, v.notes, v.concerning, now)
|
org = RPerson(
|
||||||
|
pid,
|
||||||
|
cid,
|
||||||
|
v.name,
|
||||||
|
v.address.street,
|
||||||
|
v.address.zip,
|
||||||
|
v.address.city,
|
||||||
|
v.address.country,
|
||||||
|
v.notes,
|
||||||
|
v.concerning,
|
||||||
|
now
|
||||||
|
)
|
||||||
} yield OOrganization.PersonAndContacts(org, cont)
|
} yield OOrganization.PersonAndContacts(org, cont)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -224,7 +300,18 @@ trait Conversions {
|
|||||||
v.contacts.traverse(c => newContact(c, None, pid.some))
|
v.contacts.traverse(c => newContact(c, None, pid.some))
|
||||||
for {
|
for {
|
||||||
cont <- contacts(v.id)
|
cont <- contacts(v.id)
|
||||||
org = RPerson(v.id, cid, v.name, v.address.street, v.address.zip, v.address.city, v.address.country, v.notes, v.concerning, v.created)
|
org = RPerson(
|
||||||
|
v.id,
|
||||||
|
cid,
|
||||||
|
v.name,
|
||||||
|
v.address.street,
|
||||||
|
v.address.zip,
|
||||||
|
v.address.city,
|
||||||
|
v.address.country,
|
||||||
|
v.notes,
|
||||||
|
v.concerning,
|
||||||
|
v.created
|
||||||
|
)
|
||||||
} yield OOrganization.PersonAndContacts(org, cont)
|
} yield OOrganization.PersonAndContacts(org, cont)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -233,7 +320,8 @@ trait Conversions {
|
|||||||
Contact(rc.contactId, rc.value, rc.kind)
|
Contact(rc.contactId, rc.value, rc.kind)
|
||||||
|
|
||||||
def newContact[F[_]: Sync](c: Contact, oid: Option[Ident], pid: Option[Ident]): F[RContact] =
|
def newContact[F[_]: Sync](c: Contact, oid: Option[Ident], pid: Option[Ident]): F[RContact] =
|
||||||
timeId.map { case (id, now) =>
|
timeId.map {
|
||||||
|
case (id, now) =>
|
||||||
RContact(id, c.value, c.kind, pid, oid, now)
|
RContact(id, c.value, c.kind, pid, oid, now)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -242,12 +330,33 @@ trait Conversions {
|
|||||||
User(ru.login, ru.state, None, ru.email, ru.lastLogin, ru.loginCount, ru.created)
|
User(ru.login, ru.state, None, ru.email, ru.lastLogin, ru.loginCount, ru.created)
|
||||||
|
|
||||||
def newUser[F[_]: Sync](u: User, cid: Ident): F[RUser] =
|
def newUser[F[_]: Sync](u: User, cid: Ident): F[RUser] =
|
||||||
timeId.map { case (id, now) =>
|
timeId.map {
|
||||||
RUser(id, u.login, cid, u.password.getOrElse(Password.empty), u.state, u.email, 0, None, now)
|
case (id, now) =>
|
||||||
|
RUser(
|
||||||
|
id,
|
||||||
|
u.login,
|
||||||
|
cid,
|
||||||
|
u.password.getOrElse(Password.empty),
|
||||||
|
u.state,
|
||||||
|
u.email,
|
||||||
|
0,
|
||||||
|
None,
|
||||||
|
now
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
def changeUser(u: User, cid: Ident): RUser =
|
def changeUser(u: User, cid: Ident): RUser =
|
||||||
RUser(Ident.unsafe(""), u.login, cid, u.password.getOrElse(Password.empty), u.state, u.email, u.loginCount, u.lastLogin, u.created)
|
RUser(
|
||||||
|
Ident.unsafe(""),
|
||||||
|
u.login,
|
||||||
|
cid,
|
||||||
|
u.password.getOrElse(Password.empty),
|
||||||
|
u.state,
|
||||||
|
u.email,
|
||||||
|
u.loginCount,
|
||||||
|
u.lastLogin,
|
||||||
|
u.created
|
||||||
|
)
|
||||||
|
|
||||||
// tags
|
// tags
|
||||||
|
|
||||||
@ -255,21 +364,22 @@ trait Conversions {
|
|||||||
Tag(rt.tagId, rt.name, rt.category, rt.created)
|
Tag(rt.tagId, rt.name, rt.category, rt.created)
|
||||||
|
|
||||||
def newTag[F[_]: Sync](t: Tag, cid: Ident): F[RTag] =
|
def newTag[F[_]: Sync](t: Tag, cid: Ident): F[RTag] =
|
||||||
timeId.map { case (id, now) =>
|
timeId.map {
|
||||||
|
case (id, now) =>
|
||||||
RTag(id, cid, t.name, t.category, now)
|
RTag(id, cid, t.name, t.category, now)
|
||||||
}
|
}
|
||||||
|
|
||||||
def changeTag(t: Tag, cid: Ident): RTag =
|
def changeTag(t: Tag, cid: Ident): RTag =
|
||||||
RTag(t.id, cid, t.name, t.category, t.created)
|
RTag(t.id, cid, t.name, t.category, t.created)
|
||||||
|
|
||||||
|
|
||||||
// sources
|
// sources
|
||||||
|
|
||||||
def mkSource(s: RSource): Source =
|
def mkSource(s: RSource): Source =
|
||||||
Source(s.sid, s.abbrev, s.description, s.counter, s.enabled, s.priority, s.created)
|
Source(s.sid, s.abbrev, s.description, s.counter, s.enabled, s.priority, s.created)
|
||||||
|
|
||||||
def newSource[F[_]: Sync](s: Source, cid: Ident): F[RSource] =
|
def newSource[F[_]: Sync](s: Source, cid: Ident): F[RSource] =
|
||||||
timeId.map({ case (id, now) =>
|
timeId.map({
|
||||||
|
case (id, now) =>
|
||||||
RSource(id, cid, s.abbrev, s.description, 0, s.enabled, s.priority, now)
|
RSource(id, cid, s.abbrev, s.description, 0, s.enabled, s.priority, now)
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -281,7 +391,8 @@ trait Conversions {
|
|||||||
Equipment(re.eid, re.name, re.created)
|
Equipment(re.eid, re.name, re.created)
|
||||||
|
|
||||||
def newEquipment[F[_]: Sync](e: Equipment, cid: Ident): F[REquipment] =
|
def newEquipment[F[_]: Sync](e: Equipment, cid: Ident): F[REquipment] =
|
||||||
timeId.map({ case (id, now) =>
|
timeId.map({
|
||||||
|
case (id, now) =>
|
||||||
REquipment(id, cid, e.name, now)
|
REquipment(id, cid, e.name, now)
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -298,7 +409,8 @@ trait Conversions {
|
|||||||
def basicResult(cr: JobCancelResult): BasicResult =
|
def basicResult(cr: JobCancelResult): BasicResult =
|
||||||
cr match {
|
cr match {
|
||||||
case JobCancelResult.JobNotFound => BasicResult(false, "Job not found")
|
case JobCancelResult.JobNotFound => BasicResult(false, "Job not found")
|
||||||
case JobCancelResult.CancelRequested => BasicResult(true, "Cancel was requested at the job executor")
|
case JobCancelResult.CancelRequested =>
|
||||||
|
BasicResult(true, "Cancel was requested at the job executor")
|
||||||
case JobCancelResult.Removed => BasicResult(true, "The job has been removed from the queue.")
|
case JobCancelResult.Removed => BasicResult(true, "The job has been removed from the queue.")
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -317,7 +429,8 @@ trait Conversions {
|
|||||||
def basicResult(cr: PassChangeResult): BasicResult = cr match {
|
def basicResult(cr: PassChangeResult): BasicResult = cr match {
|
||||||
case PassChangeResult.Success => BasicResult(true, "Password changed.")
|
case PassChangeResult.Success => BasicResult(true, "Password changed.")
|
||||||
case PassChangeResult.UpdateFailed => BasicResult(false, "The database update failed.")
|
case PassChangeResult.UpdateFailed => BasicResult(false, "The database update failed.")
|
||||||
case PassChangeResult.PasswordMismatch => BasicResult(false, "The current password is incorrect.")
|
case PassChangeResult.PasswordMismatch =>
|
||||||
|
BasicResult(false, "The current password is incorrect.")
|
||||||
case PassChangeResult.UserNotFound => BasicResult(false, "User not found.")
|
case PassChangeResult.UserNotFound => BasicResult(false, "User not found.")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -8,28 +8,26 @@ import org.http4s.dsl.Http4sDsl
|
|||||||
trait ResponseGenerator[F[_]] {
|
trait ResponseGenerator[F[_]] {
|
||||||
self: Http4sDsl[F] =>
|
self: Http4sDsl[F] =>
|
||||||
|
|
||||||
|
implicit final class EitherResponses[A, B](e: Either[A, B]) {
|
||||||
implicit final class EitherResponses[A,B](e: Either[A, B]) {
|
def toResponse(headers: Header*)(
|
||||||
def toResponse(headers: Header*)
|
implicit F: Applicative[F],
|
||||||
(implicit F: Applicative[F]
|
w0: EntityEncoder[F, A],
|
||||||
, w0: EntityEncoder[F, A]
|
w1: EntityEncoder[F, B]
|
||||||
, w1: EntityEncoder[F, B]): F[Response[F]] =
|
): F[Response[F]] =
|
||||||
e.fold(
|
e.fold(
|
||||||
a => UnprocessableEntity(a),
|
a => UnprocessableEntity(a),
|
||||||
b => Ok(b)
|
b => Ok(b)
|
||||||
).map(_.withHeaders(headers: _*))
|
)
|
||||||
|
.map(_.withHeaders(headers: _*))
|
||||||
}
|
}
|
||||||
|
|
||||||
implicit final class OptionResponse[A](o: Option[A]) {
|
implicit final class OptionResponse[A](o: Option[A]) {
|
||||||
def toResponse(headers: Header*)
|
def toResponse(
|
||||||
(implicit F: Applicative[F]
|
headers: Header*
|
||||||
, w0: EntityEncoder[F, A]): F[Response[F]] =
|
)(implicit F: Applicative[F], w0: EntityEncoder[F, A]): F[Response[F]] =
|
||||||
o.map(a => Ok(a)).getOrElse(NotFound()).map(_.withHeaders(headers: _*))
|
o.map(a => Ok(a)).getOrElse(NotFound()).map(_.withHeaders(headers: _*))
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
object ResponseGenerator {
|
object ResponseGenerator {}
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
|
@ -18,17 +18,18 @@ import org.http4s.headers.ETag.EntityTag
|
|||||||
object AttachmentRoutes {
|
object AttachmentRoutes {
|
||||||
|
|
||||||
def apply[F[_]: Effect](backend: BackendApp[F], user: AuthToken): HttpRoutes[F] = {
|
def apply[F[_]: Effect](backend: BackendApp[F], user: AuthToken): HttpRoutes[F] = {
|
||||||
val dsl = new Http4sDsl[F]{}
|
val dsl = new Http4sDsl[F] {}
|
||||||
import dsl._
|
import dsl._
|
||||||
|
|
||||||
def makeByteResp(data: OItem.AttachmentData[F]): F[Response[F]] = {
|
def makeByteResp(data: OItem.AttachmentData[F]): F[Response[F]] = {
|
||||||
val mt = MediaType.unsafeParse(data.meta.mimetype.asString)
|
val mt = MediaType.unsafeParse(data.meta.mimetype.asString)
|
||||||
val cntLen: Header = `Content-Length`.unsafeFromLong(data.meta.length)
|
val cntLen: Header = `Content-Length`.unsafeFromLong(data.meta.length)
|
||||||
val eTag: Header = ETag(data.meta.checksum)
|
val eTag: Header = ETag(data.meta.checksum)
|
||||||
val disp: Header = `Content-Disposition`("inline", Map("filename" -> data.ra.name.getOrElse("")))
|
val disp: Header =
|
||||||
Ok(data.data.take(data.meta.length)).
|
`Content-Disposition`("inline", Map("filename" -> data.ra.name.getOrElse("")))
|
||||||
map(r => r.withContentType(`Content-Type`(mt)).
|
Ok(data.data.take(data.meta.length)).map(r =>
|
||||||
withHeaders(cntLen, eTag, disp))
|
r.withContentType(`Content-Type`(mt)).withHeaders(cntLen, eTag, disp)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
HttpRoutes.of {
|
HttpRoutes.of {
|
||||||
@ -38,7 +39,8 @@ object AttachmentRoutes {
|
|||||||
inm = req.headers.get(`If-None-Match`).flatMap(_.tags)
|
inm = req.headers.get(`If-None-Match`).flatMap(_.tags)
|
||||||
matches = matchETag(fileData, inm)
|
matches = matchETag(fileData, inm)
|
||||||
resp <- if (matches) NotModified()
|
resp <- if (matches) NotModified()
|
||||||
else fileData.map(makeByteResp).getOrElse(NotFound(BasicResult(false, "Not found")))
|
else
|
||||||
|
fileData.map(makeByteResp).getOrElse(NotFound(BasicResult(false, "Not found")))
|
||||||
} yield resp
|
} yield resp
|
||||||
|
|
||||||
case GET -> Root / Ident(id) / "meta" =>
|
case GET -> Root / Ident(id) / "meta" =>
|
||||||
@ -50,8 +52,10 @@ object AttachmentRoutes {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private def matchETag[F[_]]( fileData: Option[OItem.AttachmentData[F]]
|
private def matchETag[F[_]](
|
||||||
, noneMatch: Option[NonEmptyList[EntityTag]]): Boolean =
|
fileData: Option[OItem.AttachmentData[F]],
|
||||||
|
noneMatch: Option[NonEmptyList[EntityTag]]
|
||||||
|
): Boolean =
|
||||||
(fileData, noneMatch) match {
|
(fileData, noneMatch) match {
|
||||||
case (Some(fd), Some(nm)) =>
|
case (Some(fd), Some(nm)) =>
|
||||||
fd.meta.checksum == nm.head.tag
|
fd.meta.checksum == nm.head.tag
|
||||||
|
@ -12,14 +12,17 @@ import org.http4s.server._
|
|||||||
|
|
||||||
object Authenticate {
|
object Authenticate {
|
||||||
|
|
||||||
def authenticateRequest[F[_]: Effect](auth: String => F[Login.Result])(req: Request[F]): F[Login.Result] =
|
def authenticateRequest[F[_]: Effect](
|
||||||
|
auth: String => F[Login.Result]
|
||||||
|
)(req: Request[F]): F[Login.Result] =
|
||||||
CookieData.authenticator(req) match {
|
CookieData.authenticator(req) match {
|
||||||
case Right(str) => auth(str)
|
case Right(str) => auth(str)
|
||||||
case Left(_) => Login.Result.invalidAuth.pure[F]
|
case Left(_) => Login.Result.invalidAuth.pure[F]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def of[F[_]: Effect](S: Login[F], cfg: Login.Config)(
|
||||||
def of[F[_]: Effect](S: Login[F], cfg: Login.Config)(pf: PartialFunction[AuthedRequest[F, AuthToken], F[Response[F]]]): HttpRoutes[F] = {
|
pf: PartialFunction[AuthedRequest[F, AuthToken], F[Response[F]]]
|
||||||
|
): HttpRoutes[F] = {
|
||||||
val dsl: Http4sDsl[F] = new Http4sDsl[F] {}
|
val dsl: Http4sDsl[F] = new Http4sDsl[F] {}
|
||||||
import dsl._
|
import dsl._
|
||||||
|
|
||||||
@ -34,7 +37,9 @@ object Authenticate {
|
|||||||
middleware(AuthedRoutes.of(pf))
|
middleware(AuthedRoutes.of(pf))
|
||||||
}
|
}
|
||||||
|
|
||||||
def apply[F[_]: Effect](S: Login[F], cfg: Login.Config)(f: AuthToken => HttpRoutes[F]): HttpRoutes[F] = {
|
def apply[F[_]: Effect](S: Login[F], cfg: Login.Config)(
|
||||||
|
f: AuthToken => HttpRoutes[F]
|
||||||
|
): HttpRoutes[F] = {
|
||||||
val dsl: Http4sDsl[F] = new Http4sDsl[F] {}
|
val dsl: Http4sDsl[F] = new Http4sDsl[F] {}
|
||||||
import dsl._
|
import dsl._
|
||||||
|
|
||||||
@ -49,6 +54,8 @@ object Authenticate {
|
|||||||
middleware(AuthedRoutes(authReq => f(authReq.context).run(authReq.req)))
|
middleware(AuthedRoutes(authReq => f(authReq.context).run(authReq.req)))
|
||||||
}
|
}
|
||||||
|
|
||||||
private def getUser[F[_]: Effect](auth: String => F[Login.Result]): Kleisli[F, Request[F], Either[String, AuthToken]] =
|
private def getUser[F[_]: Effect](
|
||||||
|
auth: String => F[Login.Result]
|
||||||
|
): Kleisli[F, Request[F], Either[String, AuthToken]] =
|
||||||
Kleisli(r => authenticateRequest(auth)(r).map(_.toEither))
|
Kleisli(r => authenticateRequest(auth)(r).map(_.toEither))
|
||||||
}
|
}
|
||||||
|
@ -25,7 +25,7 @@ object CollectiveRoutes {
|
|||||||
resp <- Ok(Conversions.mkItemInsights(ins))
|
resp <- Ok(Conversions.mkItemInsights(ins))
|
||||||
} yield resp
|
} yield resp
|
||||||
|
|
||||||
case req@POST -> Root / "settings" =>
|
case req @ POST -> Root / "settings" =>
|
||||||
for {
|
for {
|
||||||
settings <- req.as[CollectiveSettings]
|
settings <- req.as[CollectiveSettings]
|
||||||
res <- backend.collective.updateLanguage(user.account.collective, settings.language)
|
res <- backend.collective.updateLanguage(user.account.collective, settings.language)
|
||||||
|
@ -15,7 +15,7 @@ import org.http4s.dsl.Http4sDsl
|
|||||||
object EquipmentRoutes {
|
object EquipmentRoutes {
|
||||||
|
|
||||||
def apply[F[_]: Effect](backend: BackendApp[F], user: AuthToken): HttpRoutes[F] = {
|
def apply[F[_]: Effect](backend: BackendApp[F], user: AuthToken): HttpRoutes[F] = {
|
||||||
val dsl = new Http4sDsl[F]{}
|
val dsl = new Http4sDsl[F] {}
|
||||||
import dsl._
|
import dsl._
|
||||||
|
|
||||||
HttpRoutes.of {
|
HttpRoutes.of {
|
||||||
|
@ -10,15 +10,19 @@ import org.http4s.dsl.Http4sDsl
|
|||||||
object InfoRoutes {
|
object InfoRoutes {
|
||||||
|
|
||||||
def apply[F[_]: Sync](): HttpRoutes[F] = {
|
def apply[F[_]: Sync](): HttpRoutes[F] = {
|
||||||
val dsl = new Http4sDsl[F]{}
|
val dsl = new Http4sDsl[F] {}
|
||||||
import dsl._
|
import dsl._
|
||||||
HttpRoutes.of[F] {
|
HttpRoutes.of[F] {
|
||||||
case GET -> (Root / "version") =>
|
case GET -> (Root / "version") =>
|
||||||
Ok(VersionInfo(BuildInfo.version
|
Ok(
|
||||||
, BuildInfo.builtAtMillis
|
VersionInfo(
|
||||||
, BuildInfo.builtAtString
|
BuildInfo.version,
|
||||||
, BuildInfo.gitHeadCommit.getOrElse("")
|
BuildInfo.builtAtMillis,
|
||||||
, BuildInfo.gitDescribedVersion.getOrElse("")))
|
BuildInfo.builtAtString,
|
||||||
|
BuildInfo.gitHeadCommit.getOrElse(""),
|
||||||
|
BuildInfo.gitDescribedVersion.getOrElse("")
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -18,7 +18,7 @@ object ItemRoutes {
|
|||||||
private[this] val logger = getLogger
|
private[this] val logger = getLogger
|
||||||
|
|
||||||
def apply[F[_]: Effect](backend: BackendApp[F], user: AuthToken): HttpRoutes[F] = {
|
def apply[F[_]: Effect](backend: BackendApp[F], user: AuthToken): HttpRoutes[F] = {
|
||||||
val dsl = new Http4sDsl[F]{}
|
val dsl = new Http4sDsl[F] {}
|
||||||
import dsl._
|
import dsl._
|
||||||
|
|
||||||
HttpRoutes.of {
|
HttpRoutes.of {
|
||||||
@ -51,63 +51,63 @@ object ItemRoutes {
|
|||||||
resp <- Ok(Conversions.basicResult(res, "Item back to created."))
|
resp <- Ok(Conversions.basicResult(res, "Item back to created."))
|
||||||
} yield resp
|
} yield resp
|
||||||
|
|
||||||
case req@POST -> Root / Ident(id) / "tags" =>
|
case req @ POST -> Root / Ident(id) / "tags" =>
|
||||||
for {
|
for {
|
||||||
tags <- req.as[ReferenceList].map(_.items)
|
tags <- req.as[ReferenceList].map(_.items)
|
||||||
res <- backend.item.setTags(id, tags.map(_.id), user.account.collective)
|
res <- backend.item.setTags(id, tags.map(_.id), user.account.collective)
|
||||||
resp <- Ok(Conversions.basicResult(res, "Tags updated"))
|
resp <- Ok(Conversions.basicResult(res, "Tags updated"))
|
||||||
} yield resp
|
} yield resp
|
||||||
|
|
||||||
case req@POST -> Root / Ident(id) / "direction" =>
|
case req @ POST -> Root / Ident(id) / "direction" =>
|
||||||
for {
|
for {
|
||||||
dir <- req.as[DirectionValue]
|
dir <- req.as[DirectionValue]
|
||||||
res <- backend.item.setDirection(id, dir.direction, user.account.collective)
|
res <- backend.item.setDirection(id, dir.direction, user.account.collective)
|
||||||
resp <- Ok(Conversions.basicResult(res, "Direction updated"))
|
resp <- Ok(Conversions.basicResult(res, "Direction updated"))
|
||||||
} yield resp
|
} yield resp
|
||||||
|
|
||||||
case req@POST -> Root / Ident(id) / "corrOrg" =>
|
case req @ POST -> Root / Ident(id) / "corrOrg" =>
|
||||||
for {
|
for {
|
||||||
idref <- req.as[OptionalId]
|
idref <- req.as[OptionalId]
|
||||||
res <- backend.item.setCorrOrg(id, idref.id, user.account.collective)
|
res <- backend.item.setCorrOrg(id, idref.id, user.account.collective)
|
||||||
resp <- Ok(Conversions.basicResult(res, "Correspondent organization updated"))
|
resp <- Ok(Conversions.basicResult(res, "Correspondent organization updated"))
|
||||||
} yield resp
|
} yield resp
|
||||||
|
|
||||||
case req@POST -> Root / Ident(id) / "corrPerson" =>
|
case req @ POST -> Root / Ident(id) / "corrPerson" =>
|
||||||
for {
|
for {
|
||||||
idref <- req.as[OptionalId]
|
idref <- req.as[OptionalId]
|
||||||
res <- backend.item.setCorrPerson(id, idref.id, user.account.collective)
|
res <- backend.item.setCorrPerson(id, idref.id, user.account.collective)
|
||||||
resp <- Ok(Conversions.basicResult(res, "Correspondent person updated"))
|
resp <- Ok(Conversions.basicResult(res, "Correspondent person updated"))
|
||||||
} yield resp
|
} yield resp
|
||||||
|
|
||||||
case req@POST -> Root / Ident(id) / "concPerson" =>
|
case req @ POST -> Root / Ident(id) / "concPerson" =>
|
||||||
for {
|
for {
|
||||||
idref <- req.as[OptionalId]
|
idref <- req.as[OptionalId]
|
||||||
res <- backend.item.setConcPerson(id, idref.id, user.account.collective)
|
res <- backend.item.setConcPerson(id, idref.id, user.account.collective)
|
||||||
resp <- Ok(Conversions.basicResult(res, "Concerned person updated"))
|
resp <- Ok(Conversions.basicResult(res, "Concerned person updated"))
|
||||||
} yield resp
|
} yield resp
|
||||||
|
|
||||||
case req@POST -> Root / Ident(id) / "concEquipment" =>
|
case req @ POST -> Root / Ident(id) / "concEquipment" =>
|
||||||
for {
|
for {
|
||||||
idref <- req.as[OptionalId]
|
idref <- req.as[OptionalId]
|
||||||
res <- backend.item.setConcEquip(id, idref.id, user.account.collective)
|
res <- backend.item.setConcEquip(id, idref.id, user.account.collective)
|
||||||
resp <- Ok(Conversions.basicResult(res, "Concerned equipment updated"))
|
resp <- Ok(Conversions.basicResult(res, "Concerned equipment updated"))
|
||||||
} yield resp
|
} yield resp
|
||||||
|
|
||||||
case req@POST -> Root / Ident(id) / "notes" =>
|
case req @ POST -> Root / Ident(id) / "notes" =>
|
||||||
for {
|
for {
|
||||||
text <- req.as[OptionalText]
|
text <- req.as[OptionalText]
|
||||||
res <- backend.item.setNotes(id, text.text, user.account.collective)
|
res <- backend.item.setNotes(id, text.text, user.account.collective)
|
||||||
resp <- Ok(Conversions.basicResult(res, "Concerned equipment updated"))
|
resp <- Ok(Conversions.basicResult(res, "Concerned equipment updated"))
|
||||||
} yield resp
|
} yield resp
|
||||||
|
|
||||||
case req@POST -> Root / Ident(id) / "name" =>
|
case req @ POST -> Root / Ident(id) / "name" =>
|
||||||
for {
|
for {
|
||||||
text <- req.as[OptionalText]
|
text <- req.as[OptionalText]
|
||||||
res <- backend.item.setName(id, text.text.getOrElse(""), user.account.collective)
|
res <- backend.item.setName(id, text.text.getOrElse(""), user.account.collective)
|
||||||
resp <- Ok(Conversions.basicResult(res, "Concerned equipment updated"))
|
resp <- Ok(Conversions.basicResult(res, "Concerned equipment updated"))
|
||||||
} yield resp
|
} yield resp
|
||||||
|
|
||||||
case req@POST -> Root / Ident(id) / "duedate" =>
|
case req @ POST -> Root / Ident(id) / "duedate" =>
|
||||||
for {
|
for {
|
||||||
date <- req.as[OptionalDate]
|
date <- req.as[OptionalDate]
|
||||||
_ <- logger.fdebug(s"Setting item due date to ${date.date}")
|
_ <- logger.fdebug(s"Setting item due date to ${date.date}")
|
||||||
@ -115,7 +115,7 @@ object ItemRoutes {
|
|||||||
resp <- Ok(Conversions.basicResult(res, "Item due date updated"))
|
resp <- Ok(Conversions.basicResult(res, "Item due date updated"))
|
||||||
} yield resp
|
} yield resp
|
||||||
|
|
||||||
case req@POST -> Root / Ident(id) / "date" =>
|
case req @ POST -> Root / Ident(id) / "date" =>
|
||||||
for {
|
for {
|
||||||
date <- req.as[OptionalDate]
|
date <- req.as[OptionalDate]
|
||||||
_ <- logger.fdebug(s"Setting item date to ${date.date}")
|
_ <- logger.fdebug(s"Setting item date to ${date.date}")
|
||||||
|
@ -18,7 +18,7 @@ object LoginRoutes {
|
|||||||
import dsl._
|
import dsl._
|
||||||
|
|
||||||
HttpRoutes.of[F] {
|
HttpRoutes.of[F] {
|
||||||
case req@POST -> Root / "login" =>
|
case req @ POST -> Root / "login" =>
|
||||||
for {
|
for {
|
||||||
up <- req.as[UserPass]
|
up <- req.as[UserPass]
|
||||||
res <- S.loginUserPass(cfg.auth)(Login.UserPass(up.account, up.password))
|
res <- S.loginUserPass(cfg.auth)(Login.UserPass(up.account, up.password))
|
||||||
@ -33,22 +33,36 @@ object LoginRoutes {
|
|||||||
|
|
||||||
HttpRoutes.of[F] {
|
HttpRoutes.of[F] {
|
||||||
case req @ POST -> Root / "session" =>
|
case req @ POST -> Root / "session" =>
|
||||||
Authenticate.authenticateRequest(S.loginSession(cfg.auth))(req).
|
Authenticate
|
||||||
flatMap(res => makeResponse(dsl, cfg, res, ""))
|
.authenticateRequest(S.loginSession(cfg.auth))(req)
|
||||||
|
.flatMap(res => makeResponse(dsl, cfg, res, ""))
|
||||||
|
|
||||||
case POST -> Root / "logout" =>
|
case POST -> Root / "logout" =>
|
||||||
Ok().map(_.addCookie(CookieData.deleteCookie(cfg)))
|
Ok().map(_.addCookie(CookieData.deleteCookie(cfg)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
def makeResponse[F[_]: Effect](dsl: Http4sDsl[F], cfg: Config, res: Login.Result, account: String): F[Response[F]] = {
|
def makeResponse[F[_]: Effect](
|
||||||
|
dsl: Http4sDsl[F],
|
||||||
|
cfg: Config,
|
||||||
|
res: Login.Result,
|
||||||
|
account: String
|
||||||
|
): F[Response[F]] = {
|
||||||
import dsl._
|
import dsl._
|
||||||
res match {
|
res match {
|
||||||
case Login.Result.Ok(token) =>
|
case Login.Result.Ok(token) =>
|
||||||
for {
|
for {
|
||||||
cd <- AuthToken.user(token.account, cfg.auth.serverSecret).map(CookieData.apply)
|
cd <- AuthToken.user(token.account, cfg.auth.serverSecret).map(CookieData.apply)
|
||||||
resp <- Ok(AuthResult(token.account.collective.id, token.account.user.id, true, "Login successful", Some(cd.asString), cfg.auth.sessionValid.millis)).
|
resp <- Ok(
|
||||||
map(_.addCookie(cd.asCookie(cfg)))
|
AuthResult(
|
||||||
|
token.account.collective.id,
|
||||||
|
token.account.user.id,
|
||||||
|
true,
|
||||||
|
"Login successful",
|
||||||
|
Some(cd.asString),
|
||||||
|
cfg.auth.sessionValid.millis
|
||||||
|
)
|
||||||
|
).map(_.addCookie(cd.asCookie(cfg)))
|
||||||
} yield resp
|
} yield resp
|
||||||
case _ =>
|
case _ =>
|
||||||
Ok(AuthResult("", account, false, "Login failed.", None, 0L))
|
Ok(AuthResult("", account, false, "Login failed.", None, 0L))
|
||||||
|
@ -16,7 +16,7 @@ import org.http4s.dsl.Http4sDsl
|
|||||||
object OrganizationRoutes {
|
object OrganizationRoutes {
|
||||||
|
|
||||||
def apply[F[_]: Effect](backend: BackendApp[F], user: AuthToken): HttpRoutes[F] = {
|
def apply[F[_]: Effect](backend: BackendApp[F], user: AuthToken): HttpRoutes[F] = {
|
||||||
val dsl = new Http4sDsl[F]{}
|
val dsl = new Http4sDsl[F] {}
|
||||||
import dsl._
|
import dsl._
|
||||||
|
|
||||||
HttpRoutes.of {
|
HttpRoutes.of {
|
||||||
|
@ -6,9 +6,10 @@ import org.http4s.dsl.impl.OptionalQueryParamDecoderMatcher
|
|||||||
object ParamDecoder {
|
object ParamDecoder {
|
||||||
|
|
||||||
implicit val booleanDecoder: QueryParamDecoder[Boolean] =
|
implicit val booleanDecoder: QueryParamDecoder[Boolean] =
|
||||||
QueryParamDecoder.fromUnsafeCast(qp => Option(qp.value).exists(_ equalsIgnoreCase "true"))("Boolean")
|
QueryParamDecoder.fromUnsafeCast(qp => Option(qp.value).exists(_.equalsIgnoreCase("true")))(
|
||||||
|
"Boolean"
|
||||||
|
)
|
||||||
|
|
||||||
object FullQueryParamMatcher extends OptionalQueryParamDecoderMatcher[Boolean]("full")
|
object FullQueryParamMatcher extends OptionalQueryParamDecoderMatcher[Boolean]("full")
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -19,7 +19,7 @@ object PersonRoutes {
|
|||||||
private[this] val logger = getLogger
|
private[this] val logger = getLogger
|
||||||
|
|
||||||
def apply[F[_]: Effect](backend: BackendApp[F], user: AuthToken): HttpRoutes[F] = {
|
def apply[F[_]: Effect](backend: BackendApp[F], user: AuthToken): HttpRoutes[F] = {
|
||||||
val dsl = new Http4sDsl[F]{}
|
val dsl = new Http4sDsl[F] {}
|
||||||
import dsl._
|
import dsl._
|
||||||
|
|
||||||
HttpRoutes.of {
|
HttpRoutes.of {
|
||||||
|
@ -29,7 +29,7 @@ object RegisterRoutes {
|
|||||||
resp <- Ok(convert(res))
|
resp <- Ok(convert(res))
|
||||||
} yield resp
|
} yield resp
|
||||||
|
|
||||||
case req@ POST -> Root / "newinvite" =>
|
case req @ POST -> Root / "newinvite" =>
|
||||||
for {
|
for {
|
||||||
data <- req.as[GenInvite]
|
data <- req.as[GenInvite]
|
||||||
res <- backend.signup.newInvite(cfg.backend.signup)(data.password)
|
res <- backend.signup.newInvite(cfg.backend.signup)(data.password)
|
||||||
@ -47,7 +47,6 @@ object RegisterRoutes {
|
|||||||
InviteResult(false, "Password is invalid.", None)
|
InviteResult(false, "Password is invalid.", None)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def convert(r: SignupResult): BasicResult = r match {
|
def convert(r: SignupResult): BasicResult = r match {
|
||||||
case SignupResult.CollectiveExists =>
|
case SignupResult.CollectiveExists =>
|
||||||
BasicResult(false, "A collective with this name already exists.")
|
BasicResult(false, "A collective with this name already exists.")
|
||||||
@ -62,7 +61,6 @@ object RegisterRoutes {
|
|||||||
BasicResult(true, "Signup successful")
|
BasicResult(true, "Signup successful")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def convert(r: Registration): RegisterData =
|
def convert(r: Registration): RegisterData =
|
||||||
RegisterData(r.collectiveName, r.login, r.password, r.invite)
|
RegisterData(r.collectiveName, r.login, r.password, r.invite)
|
||||||
}
|
}
|
||||||
|
@ -26,7 +26,12 @@ object UploadRoutes {
|
|||||||
case req @ POST -> Root / "item" =>
|
case req @ POST -> Root / "item" =>
|
||||||
for {
|
for {
|
||||||
multipart <- req.as[Multipart[F]]
|
multipart <- req.as[Multipart[F]]
|
||||||
updata <- readMultipart(multipart, logger, Priority.High, cfg.backend.files.validMimeTypes)
|
updata <- readMultipart(
|
||||||
|
multipart,
|
||||||
|
logger,
|
||||||
|
Priority.High,
|
||||||
|
cfg.backend.files.validMimeTypes
|
||||||
|
)
|
||||||
result <- backend.upload.submit(updata, user.account)
|
result <- backend.upload.submit(updata, user.account)
|
||||||
res <- Ok(basicResult(result))
|
res <- Ok(basicResult(result))
|
||||||
} yield res
|
} yield res
|
||||||
@ -39,7 +44,7 @@ object UploadRoutes {
|
|||||||
import dsl._
|
import dsl._
|
||||||
|
|
||||||
HttpRoutes.of {
|
HttpRoutes.of {
|
||||||
case req @ POST -> Root / "item" / Ident(id)=>
|
case req @ POST -> Root / "item" / Ident(id) =>
|
||||||
for {
|
for {
|
||||||
multipart <- req.as[Multipart[F]]
|
multipart <- req.as[Multipart[F]]
|
||||||
updata <- readMultipart(multipart, logger, Priority.Low, cfg.backend.files.validMimeTypes)
|
updata <- readMultipart(multipart, logger, Priority.Low, cfg.backend.files.validMimeTypes)
|
||||||
|
@ -23,7 +23,11 @@ object UserRoutes {
|
|||||||
case req @ POST -> Root / "changePassword" =>
|
case req @ POST -> Root / "changePassword" =>
|
||||||
for {
|
for {
|
||||||
data <- req.as[PasswordChange]
|
data <- req.as[PasswordChange]
|
||||||
res <- backend.collective.changePassword(user.account, data.currentPassword, data.newPassword)
|
res <- backend.collective.changePassword(
|
||||||
|
user.account,
|
||||||
|
data.currentPassword,
|
||||||
|
data.newPassword
|
||||||
|
)
|
||||||
resp <- Ok(basicResult(res))
|
resp <- Ok(basicResult(res))
|
||||||
} yield resp
|
} yield resp
|
||||||
|
|
||||||
|
@ -8,14 +8,21 @@ import docspell.backend.signup.{Config => SignupConfig}
|
|||||||
import yamusca.imports._
|
import yamusca.imports._
|
||||||
import yamusca.implicits._
|
import yamusca.implicits._
|
||||||
|
|
||||||
case class Flags( appName: String
|
case class Flags(
|
||||||
, baseUrl: LenientUri
|
appName: String,
|
||||||
, signupMode: SignupConfig.Mode
|
baseUrl: LenientUri,
|
||||||
, docspellAssetPath: String)
|
signupMode: SignupConfig.Mode,
|
||||||
|
docspellAssetPath: String
|
||||||
|
)
|
||||||
|
|
||||||
object Flags {
|
object Flags {
|
||||||
def apply(cfg: Config): Flags =
|
def apply(cfg: Config): Flags =
|
||||||
Flags(cfg.appName, cfg.baseUrl, cfg.backend.signup.mode, s"assets/docspell-webapp/${BuildInfo.version}")
|
Flags(
|
||||||
|
cfg.appName,
|
||||||
|
cfg.baseUrl,
|
||||||
|
cfg.backend.signup.mode,
|
||||||
|
s"assets/docspell-webapp/${BuildInfo.version}"
|
||||||
|
)
|
||||||
|
|
||||||
implicit val jsonEncoder: Encoder[Flags] =
|
implicit val jsonEncoder: Encoder[Flags] =
|
||||||
deriveEncoder[Flags]
|
deriveEncoder[Flags]
|
||||||
|
@ -21,11 +21,13 @@ object TemplateRoutes {
|
|||||||
|
|
||||||
val `text/html` = new MediaType("text", "html")
|
val `text/html` = new MediaType("text", "html")
|
||||||
|
|
||||||
def apply[F[_]: Effect](blocker: Blocker, cfg: Config)(implicit C: ContextShift[F]): HttpRoutes[F] = {
|
def apply[F[_]: Effect](blocker: Blocker, cfg: Config)(
|
||||||
|
implicit C: ContextShift[F]
|
||||||
|
): HttpRoutes[F] = {
|
||||||
val indexTemplate = memo(loadResource("/index.html").flatMap(loadTemplate(_, blocker)))
|
val indexTemplate = memo(loadResource("/index.html").flatMap(loadTemplate(_, blocker)))
|
||||||
val docTemplate = memo(loadResource("/doc.html").flatMap(loadTemplate(_, blocker)))
|
val docTemplate = memo(loadResource("/doc.html").flatMap(loadTemplate(_, blocker)))
|
||||||
|
|
||||||
val dsl = new Http4sDsl[F]{}
|
val dsl = new Http4sDsl[F] {}
|
||||||
import dsl._
|
import dsl._
|
||||||
HttpRoutes.of[F] {
|
HttpRoutes.of[F] {
|
||||||
case GET -> Root / "index.html" =>
|
case GET -> Root / "index.html" =>
|
||||||
@ -41,20 +43,21 @@ object TemplateRoutes {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
def loadResource[F[_]: Sync](name: String): F[URL] = {
|
def loadResource[F[_]: Sync](name: String): F[URL] =
|
||||||
Option(getClass.getResource(name)) match {
|
Option(getClass.getResource(name)) match {
|
||||||
case None =>
|
case None =>
|
||||||
Sync[F].raiseError(new Exception("Unknown resource: "+ name))
|
Sync[F].raiseError(new Exception("Unknown resource: " + name))
|
||||||
case Some(r) =>
|
case Some(r) =>
|
||||||
r.pure[F]
|
r.pure[F]
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
def loadUrl[F[_]: Sync](url: URL, blocker: Blocker)(implicit C: ContextShift[F]): F[String] =
|
def loadUrl[F[_]: Sync](url: URL, blocker: Blocker)(implicit C: ContextShift[F]): F[String] =
|
||||||
Stream.bracket(Sync[F].delay(url.openStream))(in => Sync[F].delay(in.close())).
|
Stream
|
||||||
flatMap(in => io.readInputStream(in.pure[F], 64 * 1024, blocker, false)).
|
.bracket(Sync[F].delay(url.openStream))(in => Sync[F].delay(in.close()))
|
||||||
through(text.utf8Decode).
|
.flatMap(in => io.readInputStream(in.pure[F], 64 * 1024, blocker, false))
|
||||||
compile.fold("")(_ + _)
|
.through(text.utf8Decode)
|
||||||
|
.compile
|
||||||
|
.fold("")(_ + _)
|
||||||
|
|
||||||
def parseTemplate[F[_]: Sync](str: String): F[Template] =
|
def parseTemplate[F[_]: Sync](str: String): F[Template] =
|
||||||
Sync[F].delay {
|
Sync[F].delay {
|
||||||
@ -64,47 +67,54 @@ object TemplateRoutes {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
def loadTemplate[F[_]: Sync](url: URL, blocker: Blocker)(implicit C: ContextShift[F]): F[Template] = {
|
def loadTemplate[F[_]: Sync](url: URL, blocker: Blocker)(
|
||||||
loadUrl[F](url, blocker).flatMap(s => parseTemplate(s)).
|
implicit C: ContextShift[F]
|
||||||
map(t => {
|
): F[Template] =
|
||||||
|
loadUrl[F](url, blocker).flatMap(s => parseTemplate(s)).map { t =>
|
||||||
logger.info(s"Compiled template $url")
|
logger.info(s"Compiled template $url")
|
||||||
t
|
t
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
case class DocData(swaggerRoot: String, openapiSpec: String)
|
case class DocData(swaggerRoot: String, openapiSpec: String)
|
||||||
object DocData {
|
object DocData {
|
||||||
|
|
||||||
def apply(): DocData =
|
def apply(): DocData =
|
||||||
DocData("/app/assets" + Webjars.swaggerui, s"/app/assets/${BuildInfo.name}/${BuildInfo.version}/docspell-openapi.yml")
|
DocData(
|
||||||
|
"/app/assets" + Webjars.swaggerui,
|
||||||
|
s"/app/assets/${BuildInfo.name}/${BuildInfo.version}/docspell-openapi.yml"
|
||||||
|
)
|
||||||
|
|
||||||
implicit def yamuscaValueConverter: ValueConverter[DocData] =
|
implicit def yamuscaValueConverter: ValueConverter[DocData] =
|
||||||
ValueConverter.deriveConverter[DocData]
|
ValueConverter.deriveConverter[DocData]
|
||||||
}
|
}
|
||||||
|
|
||||||
case class IndexData(flags: Flags
|
case class IndexData(
|
||||||
, cssUrls: Seq[String]
|
flags: Flags,
|
||||||
, jsUrls: Seq[String]
|
cssUrls: Seq[String],
|
||||||
, faviconBase: String
|
jsUrls: Seq[String],
|
||||||
, appExtraJs: String
|
faviconBase: String,
|
||||||
, flagsJson: String)
|
appExtraJs: String,
|
||||||
|
flagsJson: String
|
||||||
|
)
|
||||||
|
|
||||||
object IndexData {
|
object IndexData {
|
||||||
|
|
||||||
def apply(cfg: Config): IndexData =
|
def apply(cfg: Config): IndexData =
|
||||||
IndexData(Flags(cfg)
|
IndexData(
|
||||||
, Seq(
|
Flags(cfg),
|
||||||
|
Seq(
|
||||||
"/app/assets" + Webjars.semanticui + "/semantic.min.css",
|
"/app/assets" + Webjars.semanticui + "/semantic.min.css",
|
||||||
s"/app/assets/docspell-webapp/${BuildInfo.version}/docspell.css"
|
s"/app/assets/docspell-webapp/${BuildInfo.version}/docspell.css"
|
||||||
)
|
),
|
||||||
, Seq(
|
Seq(
|
||||||
"/app/assets" + Webjars.jquery + "/jquery.min.js",
|
"/app/assets" + Webjars.jquery + "/jquery.min.js",
|
||||||
"/app/assets" + Webjars.semanticui + "/semantic.min.js",
|
"/app/assets" + Webjars.semanticui + "/semantic.min.js",
|
||||||
s"/app/assets/docspell-webapp/${BuildInfo.version}/docspell-app.js"
|
s"/app/assets/docspell-webapp/${BuildInfo.version}/docspell-app.js"
|
||||||
|
),
|
||||||
|
s"/app/assets/docspell-webapp/${BuildInfo.version}/favicon",
|
||||||
|
s"/app/assets/docspell-webapp/${BuildInfo.version}/docspell.js",
|
||||||
|
Flags(cfg).asJson.spaces2
|
||||||
)
|
)
|
||||||
, s"/app/assets/docspell-webapp/${BuildInfo.version}/favicon"
|
|
||||||
, s"/app/assets/docspell-webapp/${BuildInfo.version}/docspell.js"
|
|
||||||
, Flags(cfg).asJson.spaces2 )
|
|
||||||
|
|
||||||
implicit def yamuscaValueConverter: ValueConverter[IndexData] =
|
implicit def yamuscaValueConverter: ValueConverter[IndexData] =
|
||||||
ValueConverter.deriveConverter[IndexData]
|
ValueConverter.deriveConverter[IndexData]
|
||||||
@ -116,10 +126,10 @@ object TemplateRoutes {
|
|||||||
Option(ref.get) match {
|
Option(ref.get) match {
|
||||||
case Some(a) => a.pure[F]
|
case Some(a) => a.pure[F]
|
||||||
case None =>
|
case None =>
|
||||||
fa.map(a => {
|
fa.map { a =>
|
||||||
ref.set(a)
|
ref.set(a)
|
||||||
a
|
a
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -9,7 +9,7 @@ import org.http4s.server.staticcontent.WebjarService.{WebjarAsset, Config => Web
|
|||||||
|
|
||||||
object WebjarRoutes {
|
object WebjarRoutes {
|
||||||
|
|
||||||
def appRoutes[F[_]: Effect](blocker: Blocker)(implicit C: ContextShift[F]): HttpRoutes[F] = {
|
def appRoutes[F[_]: Effect](blocker: Blocker)(implicit C: ContextShift[F]): HttpRoutes[F] =
|
||||||
webjarService(
|
webjarService(
|
||||||
WebjarConfig(
|
WebjarConfig(
|
||||||
filter = assetFilter,
|
filter = assetFilter,
|
||||||
@ -17,10 +17,23 @@ object WebjarRoutes {
|
|||||||
cacheStrategy = NoopCacheStrategy[F]
|
cacheStrategy = NoopCacheStrategy[F]
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
|
||||||
|
|
||||||
def assetFilter(asset: WebjarAsset): Boolean =
|
def assetFilter(asset: WebjarAsset): Boolean =
|
||||||
List(".js", ".css", ".html", ".json", ".jpg", ".png", ".eot", ".woff", ".woff2", ".svg", ".otf", ".ttf", ".yml", ".xml").
|
List(
|
||||||
exists(e => asset.asset.endsWith(e))
|
".js",
|
||||||
|
".css",
|
||||||
|
".html",
|
||||||
|
".json",
|
||||||
|
".jpg",
|
||||||
|
".png",
|
||||||
|
".eot",
|
||||||
|
".woff",
|
||||||
|
".woff2",
|
||||||
|
".svg",
|
||||||
|
".otf",
|
||||||
|
".ttf",
|
||||||
|
".yml",
|
||||||
|
".xml"
|
||||||
|
).exists(e => asset.asset.endsWith(e))
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -2,10 +2,7 @@ package docspell.store
|
|||||||
|
|
||||||
import docspell.common.LenientUri
|
import docspell.common.LenientUri
|
||||||
|
|
||||||
case class JdbcConfig(url: LenientUri
|
case class JdbcConfig(url: LenientUri, user: String, password: String) {
|
||||||
, user: String
|
|
||||||
, password: String
|
|
||||||
) {
|
|
||||||
|
|
||||||
val dbmsName: Option[String] =
|
val dbmsName: Option[String] =
|
||||||
JdbcConfig.extractDbmsName(url)
|
JdbcConfig.extractDbmsName(url)
|
||||||
|
@ -22,16 +22,20 @@ trait Store[F[_]] {
|
|||||||
|
|
||||||
object Store {
|
object Store {
|
||||||
|
|
||||||
def create[F[_]: Effect: ContextShift](jdbc: JdbcConfig
|
def create[F[_]: Effect: ContextShift](
|
||||||
, connectEC: ExecutionContext
|
jdbc: JdbcConfig,
|
||||||
, blocker: Blocker): Resource[F, Store[F]] = {
|
connectEC: ExecutionContext,
|
||||||
|
blocker: Blocker
|
||||||
|
): Resource[F, Store[F]] = {
|
||||||
|
|
||||||
val hxa = HikariTransactor.newHikariTransactor[F](jdbc.driverClass
|
val hxa = HikariTransactor.newHikariTransactor[F](
|
||||||
, jdbc.url.asString
|
jdbc.driverClass,
|
||||||
, jdbc.user
|
jdbc.url.asString,
|
||||||
, jdbc.password
|
jdbc.user,
|
||||||
, connectEC
|
jdbc.password,
|
||||||
, blocker)
|
connectEC,
|
||||||
|
blocker
|
||||||
|
)
|
||||||
|
|
||||||
for {
|
for {
|
||||||
xa <- hxa
|
xa <- hxa
|
||||||
|
@ -21,7 +21,9 @@ trait DoobieMeta {
|
|||||||
})
|
})
|
||||||
|
|
||||||
def jsonMeta[A](implicit d: Decoder[A], e: Encoder[A]): Meta[A] =
|
def jsonMeta[A](implicit d: Decoder[A], e: Encoder[A]): Meta[A] =
|
||||||
Meta[String].imap(str => str.parseJsonAs[A].fold(ex => throw ex, identity))(a => e.apply(a).noSpaces)
|
Meta[String].imap(str => str.parseJsonAs[A].fold(ex => throw ex, identity))(a =>
|
||||||
|
e.apply(a).noSpaces
|
||||||
|
)
|
||||||
|
|
||||||
implicit val metaCollectiveState: Meta[CollectiveState] =
|
implicit val metaCollectiveState: Meta[CollectiveState] =
|
||||||
Meta[String].imap(CollectiveState.unsafe)(CollectiveState.asString)
|
Meta[String].imap(CollectiveState.unsafe)(CollectiveState.asString)
|
||||||
@ -45,7 +47,9 @@ trait DoobieMeta {
|
|||||||
Meta[String].imap(JobState.unsafe)(_.name)
|
Meta[String].imap(JobState.unsafe)(_.name)
|
||||||
|
|
||||||
implicit val metaDirection: Meta[Direction] =
|
implicit val metaDirection: Meta[Direction] =
|
||||||
Meta[Boolean].imap(flag => if (flag) Direction.Incoming: Direction else Direction.Outgoing: Direction)(d => Direction.isIncoming(d))
|
Meta[Boolean].imap(flag =>
|
||||||
|
if (flag) Direction.Incoming: Direction else Direction.Outgoing: Direction
|
||||||
|
)(d => Direction.isIncoming(d))
|
||||||
|
|
||||||
implicit val metaPriority: Meta[Priority] =
|
implicit val metaPriority: Meta[Priority] =
|
||||||
Meta[Int].imap(Priority.fromInt)(Priority.toInt)
|
Meta[Int].imap(Priority.fromInt)(Priority.toInt)
|
||||||
|
@ -19,7 +19,9 @@ trait DoobieSyntax {
|
|||||||
commas(fa :: fas.toList)
|
commas(fa :: fas.toList)
|
||||||
|
|
||||||
def and(fs: Seq[Fragment]): Fragment =
|
def and(fs: Seq[Fragment]): Fragment =
|
||||||
Fragment.const(" (") ++ fs.filter(f => !isEmpty(f)).reduce(_ ++ Fragment.const(" AND ") ++ _) ++ Fragment.const(") ")
|
Fragment.const(" (") ++ fs
|
||||||
|
.filter(f => !isEmpty(f))
|
||||||
|
.reduce(_ ++ Fragment.const(" AND ") ++ _) ++ Fragment.const(") ")
|
||||||
|
|
||||||
def and(f0: Fragment, fs: Fragment*): Fragment =
|
def and(f0: Fragment, fs: Fragment*): Fragment =
|
||||||
and(f0 :: fs.toList)
|
and(f0 :: fs.toList)
|
||||||
@ -48,8 +50,9 @@ trait DoobieSyntax {
|
|||||||
|
|
||||||
def insertRows(table: Fragment, cols: List[Column], vals: List[Fragment]): Fragment =
|
def insertRows(table: Fragment, cols: List[Column], vals: List[Fragment]): Fragment =
|
||||||
Fragment.const("INSERT INTO ") ++ table ++ Fragment.const(" (") ++
|
Fragment.const("INSERT INTO ") ++ table ++ Fragment.const(" (") ++
|
||||||
commas(cols.map(_.f)) ++ Fragment.const(") VALUES ") ++ commas(vals.map(f => sql"(" ++ f ++ sql")"))
|
commas(cols.map(_.f)) ++ Fragment.const(") VALUES ") ++ commas(
|
||||||
|
vals.map(f => sql"(" ++ f ++ sql")")
|
||||||
|
)
|
||||||
|
|
||||||
def selectSimple(cols: Seq[Column], table: Fragment, where: Fragment): Fragment =
|
def selectSimple(cols: Seq[Column], table: Fragment, where: Fragment): Fragment =
|
||||||
selectSimple(commas(cols.map(_.f)), table, where)
|
selectSimple(commas(cols.map(_.f)), table, where)
|
||||||
@ -62,7 +65,6 @@ trait DoobieSyntax {
|
|||||||
Fragment.const("SELECT DISTINCT(") ++ commas(cols.map(_.f)) ++
|
Fragment.const("SELECT DISTINCT(") ++ commas(cols.map(_.f)) ++
|
||||||
Fragment.const(") FROM ") ++ table ++ this.where(where)
|
Fragment.const(") FROM ") ++ table ++ this.where(where)
|
||||||
|
|
||||||
|
|
||||||
// def selectJoinCollective(cols: Seq[Column], fkCid: Column, table: Fragment, wh: Fragment): Fragment =
|
// def selectJoinCollective(cols: Seq[Column], fkCid: Column, table: Fragment, wh: Fragment): Fragment =
|
||||||
// selectSimple(cols.map(_.prefix("a"))
|
// selectSimple(cols.map(_.prefix("a"))
|
||||||
// , table ++ fr"a," ++ RCollective.table ++ fr"b"
|
// , table ++ fr"a," ++ RCollective.table ++ fr"b"
|
||||||
@ -70,11 +72,12 @@ trait DoobieSyntax {
|
|||||||
// else and(wh, fkCid.prefix("a") is RCollective.Columns.id.prefix("b")))
|
// else and(wh, fkCid.prefix("a") is RCollective.Columns.id.prefix("b")))
|
||||||
|
|
||||||
def selectCount(col: Column, table: Fragment, where: Fragment): Fragment =
|
def selectCount(col: Column, table: Fragment, where: Fragment): Fragment =
|
||||||
Fragment.const("SELECT COUNT(") ++ col.f ++ Fragment.const(") FROM ") ++ table ++ this.where(where)
|
Fragment.const("SELECT COUNT(") ++ col.f ++ Fragment.const(") FROM ") ++ table ++ this.where(
|
||||||
|
where
|
||||||
|
)
|
||||||
|
|
||||||
def deleteFrom(table: Fragment, where: Fragment): Fragment = {
|
def deleteFrom(table: Fragment, where: Fragment): Fragment =
|
||||||
fr"DELETE FROM" ++ table ++ this.where(where)
|
fr"DELETE FROM" ++ table ++ this.where(where)
|
||||||
}
|
|
||||||
|
|
||||||
def withCTE(ps: (String, Fragment)*): Fragment = {
|
def withCTE(ps: (String, Fragment)*): Fragment = {
|
||||||
val subsel: Seq[Fragment] = ps.map(p => Fragment.const(p._1) ++ fr"AS (" ++ p._2 ++ fr")")
|
val subsel: Seq[Fragment] = ps.map(p => Fragment.const(p._1) ++ fr"AS (" ++ p._2 ++ fr")")
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
package docspell.store.impl
|
package docspell.store.impl
|
||||||
|
|
||||||
|
object Implicits extends DoobieMeta with DoobieSyntax
|
||||||
object Implicits extends DoobieMeta
|
|
||||||
with DoobieSyntax
|
|
||||||
|
@ -10,7 +10,8 @@ import doobie._
|
|||||||
import doobie.implicits._
|
import doobie.implicits._
|
||||||
|
|
||||||
final class StoreImpl[F[_]: Effect](jdbc: JdbcConfig, xa: Transactor[F]) extends Store[F] {
|
final class StoreImpl[F[_]: Effect](jdbc: JdbcConfig, xa: Transactor[F]) extends Store[F] {
|
||||||
val bitpeaceCfg = BitpeaceConfig("filemeta", "filechunk", TikaMimetypeDetect, Ident.randomId[F].map(_.id))
|
val bitpeaceCfg =
|
||||||
|
BitpeaceConfig("filemeta", "filechunk", TikaMimetypeDetect, Ident.randomId[F].map(_.id))
|
||||||
|
|
||||||
def migrate: F[Int] =
|
def migrate: F[Int] =
|
||||||
FlywayMigrate.run[F](jdbc)
|
FlywayMigrate.run[F](jdbc)
|
||||||
@ -24,14 +25,14 @@ final class StoreImpl[F[_]: Effect](jdbc: JdbcConfig, xa: Transactor[F]) extends
|
|||||||
def bitpeace: Bitpeace[F] =
|
def bitpeace: Bitpeace[F] =
|
||||||
Bitpeace(bitpeaceCfg, xa)
|
Bitpeace(bitpeaceCfg, xa)
|
||||||
|
|
||||||
def add(insert: ConnectionIO[Int], exists: ConnectionIO[Boolean]): F[AddResult] = {
|
def add(insert: ConnectionIO[Int], exists: ConnectionIO[Boolean]): F[AddResult] =
|
||||||
for {
|
for {
|
||||||
save <- transact(insert).attempt
|
save <- transact(insert).attempt
|
||||||
exist <- save.swap.traverse(ex => transact(exists).map(b => (ex, b)))
|
exist <- save.swap.traverse(ex => transact(exists).map(b => (ex, b)))
|
||||||
} yield exist.swap match {
|
} yield exist.swap match {
|
||||||
case Right(_) => AddResult.Success
|
case Right(_) => AddResult.Success
|
||||||
case Left((_, true)) => AddResult.EntityExists("Adding failed, because the entity already exists.")
|
case Left((_, true)) =>
|
||||||
|
AddResult.EntityExists("Adding failed, because the entity already exists.")
|
||||||
case Left((ex, _)) => AddResult.Failure(ex)
|
case Left((ex, _)) => AddResult.Failure(ex)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -20,11 +20,12 @@ object FlywayMigrate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
logger.info(s"Using migration locations: $locations")
|
logger.info(s"Using migration locations: $locations")
|
||||||
val fw = Flyway.configure().
|
val fw = Flyway
|
||||||
cleanDisabled(true).
|
.configure()
|
||||||
dataSource(jdbc.url.asString, jdbc.user, jdbc.password).
|
.cleanDisabled(true)
|
||||||
locations(locations: _*).
|
.dataSource(jdbc.url.asString, jdbc.user, jdbc.password)
|
||||||
load()
|
.locations(locations: _*)
|
||||||
|
.load()
|
||||||
|
|
||||||
fw.repair()
|
fw.repair()
|
||||||
fw.migrate()
|
fw.migrate()
|
||||||
|
@ -18,7 +18,7 @@ trait ONode[F[_]] {
|
|||||||
object ONode {
|
object ONode {
|
||||||
private[this] val logger = getLogger
|
private[this] val logger = getLogger
|
||||||
|
|
||||||
def apply[F[_] : Effect](store: Store[F]): Resource[F, ONode[F]] =
|
def apply[F[_]: Effect](store: Store[F]): Resource[F, ONode[F]] =
|
||||||
Resource.pure(new ONode[F] {
|
Resource.pure(new ONode[F] {
|
||||||
|
|
||||||
def register(appId: Ident, nodeType: NodeType, uri: LenientUri): F[Unit] =
|
def register(appId: Ident, nodeType: NodeType, uri: LenientUri): F[Unit] =
|
||||||
|
@ -12,40 +12,39 @@ import docspell.store.records.{RAttachment, RAttachmentMeta, RItem}
|
|||||||
|
|
||||||
object QAttachment {
|
object QAttachment {
|
||||||
|
|
||||||
def deleteById[F[_]: Sync](store: Store[F])(attachId: Ident, coll: Ident): F[Int] = {
|
def deleteById[F[_]: Sync](store: Store[F])(attachId: Ident, coll: Ident): F[Int] =
|
||||||
for {
|
for {
|
||||||
raOpt <- store.transact(RAttachment.findByIdAndCollective(attachId, coll))
|
raOpt <- store.transact(RAttachment.findByIdAndCollective(attachId, coll))
|
||||||
n <- raOpt.traverse(_ => store.transact(RAttachment.delete(attachId)))
|
n <- raOpt.traverse(_ => store.transact(RAttachment.delete(attachId)))
|
||||||
f <- Stream.emit(raOpt).
|
f <- Stream
|
||||||
unNoneTerminate.
|
.emit(raOpt)
|
||||||
map(_.fileId.id).
|
.unNoneTerminate
|
||||||
flatMap(store.bitpeace.delete).
|
.map(_.fileId.id)
|
||||||
compile.last
|
.flatMap(store.bitpeace.delete)
|
||||||
|
.compile
|
||||||
|
.last
|
||||||
} yield n.getOrElse(0) + f.map(_ => 1).getOrElse(0)
|
} yield n.getOrElse(0) + f.map(_ => 1).getOrElse(0)
|
||||||
}
|
|
||||||
|
|
||||||
def deleteAttachment[F[_]: Sync](store: Store[F])(ra: RAttachment): F[Int] = {
|
def deleteAttachment[F[_]: Sync](store: Store[F])(ra: RAttachment): F[Int] =
|
||||||
for {
|
for {
|
||||||
n <- store.transact(RAttachment.delete(ra.id))
|
n <- store.transact(RAttachment.delete(ra.id))
|
||||||
f <- Stream.emit(ra.fileId.id).
|
f <- Stream.emit(ra.fileId.id).flatMap(store.bitpeace.delete).compile.last
|
||||||
flatMap(store.bitpeace.delete).
|
|
||||||
compile.last
|
|
||||||
} yield n + f.map(_ => 1).getOrElse(0)
|
} yield n + f.map(_ => 1).getOrElse(0)
|
||||||
}
|
|
||||||
|
|
||||||
def deleteItemAttachments[F[_]: Sync](store: Store[F])(itemId: Ident, coll: Ident): F[Int] = {
|
def deleteItemAttachments[F[_]: Sync](store: Store[F])(itemId: Ident, coll: Ident): F[Int] =
|
||||||
for {
|
for {
|
||||||
ras <- store.transact(RAttachment.findByItemAndCollective(itemId, coll))
|
ras <- store.transact(RAttachment.findByItemAndCollective(itemId, coll))
|
||||||
ns <- ras.traverse(deleteAttachment[F](store))
|
ns <- ras.traverse(deleteAttachment[F](store))
|
||||||
} yield ns.sum
|
} yield ns.sum
|
||||||
}
|
|
||||||
|
|
||||||
def getMetaProposals(itemId: Ident, coll: Ident): ConnectionIO[MetaProposalList] = {
|
def getMetaProposals(itemId: Ident, coll: Ident): ConnectionIO[MetaProposalList] = {
|
||||||
val AC = RAttachment.Columns
|
val AC = RAttachment.Columns
|
||||||
val MC = RAttachmentMeta.Columns
|
val MC = RAttachmentMeta.Columns
|
||||||
val IC = RItem.Columns
|
val IC = RItem.Columns
|
||||||
|
|
||||||
val q = fr"SELECT" ++ MC.proposals.prefix("m").f ++ fr"FROM" ++ RAttachmentMeta.table ++ fr"m" ++
|
val q = fr"SELECT" ++ MC.proposals
|
||||||
|
.prefix("m")
|
||||||
|
.f ++ fr"FROM" ++ RAttachmentMeta.table ++ fr"m" ++
|
||||||
fr"INNER JOIN" ++ RAttachment.table ++ fr"a ON" ++ AC.id.prefix("a").is(MC.id.prefix("m")) ++
|
fr"INNER JOIN" ++ RAttachment.table ++ fr"a ON" ++ AC.id.prefix("a").is(MC.id.prefix("m")) ++
|
||||||
fr"INNER JOIN" ++ RItem.table ++ fr"i ON" ++ AC.itemId.prefix("a").is(IC.id.prefix("i")) ++
|
fr"INNER JOIN" ++ RItem.table ++ fr"i ON" ++ AC.itemId.prefix("a").is(IC.id.prefix("i")) ++
|
||||||
fr"WHERE" ++ and(AC.itemId.prefix("a").is(itemId), IC.cid.prefix("i").is(coll))
|
fr"WHERE" ++ and(AC.itemId.prefix("a").is(itemId), IC.cid.prefix("i").is(coll))
|
||||||
@ -55,15 +54,22 @@ object QAttachment {
|
|||||||
} yield MetaProposalList.flatten(ml)
|
} yield MetaProposalList.flatten(ml)
|
||||||
}
|
}
|
||||||
|
|
||||||
def getAttachmentMeta(attachId: Ident, collective: Ident): ConnectionIO[Option[RAttachmentMeta]] = {
|
def getAttachmentMeta(
|
||||||
|
attachId: Ident,
|
||||||
|
collective: Ident
|
||||||
|
): ConnectionIO[Option[RAttachmentMeta]] = {
|
||||||
val AC = RAttachment.Columns
|
val AC = RAttachment.Columns
|
||||||
val MC = RAttachmentMeta.Columns
|
val MC = RAttachmentMeta.Columns
|
||||||
val IC = RItem.Columns
|
val IC = RItem.Columns
|
||||||
|
|
||||||
val q = fr"SELECT" ++ commas(MC.all.map(_.prefix("m").f)) ++ fr"FROM" ++ RItem.table ++ fr"i" ++
|
val q = fr"SELECT" ++ commas(MC.all.map(_.prefix("m").f)) ++ fr"FROM" ++ RItem.table ++ fr"i" ++
|
||||||
fr"INNER JOIN" ++ RAttachment.table ++ fr"a ON" ++ IC.id.prefix("i").is(AC.itemId.prefix("a")) ++
|
fr"INNER JOIN" ++ RAttachment.table ++ fr"a ON" ++ IC.id
|
||||||
fr"INNER JOIN" ++ RAttachmentMeta.table ++ fr"m ON" ++ AC.id.prefix("a").is(MC.id.prefix("m")) ++
|
.prefix("i")
|
||||||
fr"WHERE" ++ and(AC.id.prefix("a") is attachId, IC.cid.prefix("i") is collective)
|
.is(AC.itemId.prefix("a")) ++
|
||||||
|
fr"INNER JOIN" ++ RAttachmentMeta.table ++ fr"m ON" ++ AC.id
|
||||||
|
.prefix("a")
|
||||||
|
.is(MC.id.prefix("m")) ++
|
||||||
|
fr"WHERE" ++ and(AC.id.prefix("a").is(attachId), IC.cid.prefix("i").is(collective))
|
||||||
|
|
||||||
q.query[RAttachmentMeta].option
|
q.query[RAttachmentMeta].option
|
||||||
}
|
}
|
||||||
|
@ -8,27 +8,35 @@ import docspell.store.records.{RAttachment, RItem, RTag, RTagItem}
|
|||||||
|
|
||||||
object QCollective {
|
object QCollective {
|
||||||
|
|
||||||
case class InsightData( incoming: Int
|
case class InsightData(incoming: Int, outgoing: Int, bytes: Long, tags: Map[String, Int])
|
||||||
, outgoing: Int
|
|
||||||
, bytes: Long
|
|
||||||
, tags: Map[String, Int])
|
|
||||||
|
|
||||||
def getInsights(coll: Ident): ConnectionIO[InsightData] = {
|
def getInsights(coll: Ident): ConnectionIO[InsightData] = {
|
||||||
val IC = RItem.Columns
|
val IC = RItem.Columns
|
||||||
val AC = RAttachment.Columns
|
val AC = RAttachment.Columns
|
||||||
val TC = RTag.Columns
|
val TC = RTag.Columns
|
||||||
val RC = RTagItem.Columns
|
val RC = RTagItem.Columns
|
||||||
val q0 = selectCount(IC.id, RItem.table, and(IC.cid is coll, IC.incoming is Direction.incoming)).
|
val q0 = selectCount(
|
||||||
query[Int].unique
|
IC.id,
|
||||||
val q1 = selectCount(IC.id, RItem.table, and(IC.cid is coll, IC.incoming is Direction.outgoing)).
|
RItem.table,
|
||||||
query[Int].unique
|
and(IC.cid.is(coll), IC.incoming.is(Direction.incoming))
|
||||||
|
).query[Int].unique
|
||||||
|
val q1 = selectCount(
|
||||||
|
IC.id,
|
||||||
|
RItem.table,
|
||||||
|
and(IC.cid.is(coll), IC.incoming.is(Direction.outgoing))
|
||||||
|
).query[Int].unique
|
||||||
|
|
||||||
val q2 = fr"SELECT sum(m.length) FROM" ++ RItem.table ++ fr"i" ++
|
val q2 = fr"SELECT sum(m.length) FROM" ++ RItem.table ++ fr"i" ++
|
||||||
fr"INNER JOIN" ++ RAttachment.table ++ fr"a ON" ++ AC.itemId.prefix("a").is(IC.id.prefix("i")) ++
|
fr"INNER JOIN" ++ RAttachment.table ++ fr"a ON" ++ AC.itemId
|
||||||
|
.prefix("a")
|
||||||
|
.is(IC.id.prefix("i")) ++
|
||||||
fr"INNER JOIN filemeta m ON m.id =" ++ AC.fileId.prefix("a").f ++
|
fr"INNER JOIN filemeta m ON m.id =" ++ AC.fileId.prefix("a").f ++
|
||||||
fr"WHERE" ++ IC.cid.is(coll)
|
fr"WHERE" ++ IC.cid.is(coll)
|
||||||
|
|
||||||
val q3 = fr"SELECT" ++ commas(TC.name.prefix("t").f,fr"count(" ++ RC.itemId.prefix("r").f ++ fr")") ++
|
val q3 = fr"SELECT" ++ commas(
|
||||||
|
TC.name.prefix("t").f,
|
||||||
|
fr"count(" ++ RC.itemId.prefix("r").f ++ fr")"
|
||||||
|
) ++
|
||||||
fr"FROM" ++ RTagItem.table ++ fr"r" ++
|
fr"FROM" ++ RTagItem.table ++ fr"r" ++
|
||||||
fr"INNER JOIN" ++ RTag.table ++ fr"t ON" ++ RC.tagId.prefix("r").is(TC.tid.prefix("t")) ++
|
fr"INNER JOIN" ++ RTag.table ++ fr"t ON" ++ RC.tagId.prefix("r").is(TC.tid.prefix("t")) ++
|
||||||
fr"WHERE" ++ TC.cid.prefix("t").is(coll) ++
|
fr"WHERE" ++ TC.cid.prefix("t").is(coll) ++
|
||||||
|
@ -13,16 +13,18 @@ import docspell.store.impl.Implicits._
|
|||||||
import org.log4s._
|
import org.log4s._
|
||||||
|
|
||||||
object QItem {
|
object QItem {
|
||||||
private [this] val logger = getLogger
|
private[this] val logger = getLogger
|
||||||
|
|
||||||
case class ItemData( item: RItem
|
case class ItemData(
|
||||||
, corrOrg: Option[ROrganization]
|
item: RItem,
|
||||||
, corrPerson: Option[RPerson]
|
corrOrg: Option[ROrganization],
|
||||||
, concPerson: Option[RPerson]
|
corrPerson: Option[RPerson],
|
||||||
, concEquip: Option[REquipment]
|
concPerson: Option[RPerson],
|
||||||
, inReplyTo: Option[IdRef]
|
concEquip: Option[REquipment],
|
||||||
, tags: Vector[RTag]
|
inReplyTo: Option[IdRef],
|
||||||
, attachments: Vector[(RAttachment, FileMeta)]) {
|
tags: Vector[RTag],
|
||||||
|
attachments: Vector[(RAttachment, FileMeta)]
|
||||||
|
) {
|
||||||
|
|
||||||
def filterCollective(coll: Ident): Option[ItemData] =
|
def filterCollective(coll: Ident): Option[ItemData] =
|
||||||
if (item.cid == coll) Some(this) else None
|
if (item.cid == coll) Some(this) else None
|
||||||
@ -37,14 +39,35 @@ object QItem {
|
|||||||
val ICC = List(RItem.Columns.id, RItem.Columns.name).map(_.prefix("ref"))
|
val ICC = List(RItem.Columns.id, RItem.Columns.name).map(_.prefix("ref"))
|
||||||
|
|
||||||
val cq = selectSimple(IC ++ OC ++ P0C ++ P1C ++ EC ++ ICC, RItem.table ++ fr"i", Fragment.empty) ++
|
val cq = selectSimple(IC ++ OC ++ P0C ++ P1C ++ EC ++ ICC, RItem.table ++ fr"i", Fragment.empty) ++
|
||||||
fr"LEFT JOIN" ++ ROrganization.table ++ fr"o ON" ++ RItem.Columns.corrOrg.prefix("i").is(ROrganization.Columns.oid.prefix("o")) ++
|
fr"LEFT JOIN" ++ ROrganization.table ++ fr"o ON" ++ RItem.Columns.corrOrg
|
||||||
fr"LEFT JOIN" ++ RPerson.table ++ fr"p0 ON" ++ RItem.Columns.corrPerson.prefix("i").is(RPerson.Columns.pid.prefix("p0")) ++
|
.prefix("i")
|
||||||
fr"LEFT JOIN" ++ RPerson.table ++ fr"p1 ON" ++ RItem.Columns.concPerson.prefix("i").is(RPerson.Columns.pid.prefix("p1")) ++
|
.is(ROrganization.Columns.oid.prefix("o")) ++
|
||||||
fr"LEFT JOIN" ++ REquipment.table ++ fr"e ON" ++ RItem.Columns.concEquipment.prefix("i").is(REquipment.Columns.eid.prefix("e")) ++
|
fr"LEFT JOIN" ++ RPerson.table ++ fr"p0 ON" ++ RItem.Columns.corrPerson
|
||||||
fr"LEFT JOIN" ++ RItem.table ++ fr"ref ON" ++ RItem.Columns.inReplyTo.prefix("i").is(RItem.Columns.id.prefix("ref")) ++
|
.prefix("i")
|
||||||
|
.is(RPerson.Columns.pid.prefix("p0")) ++
|
||||||
|
fr"LEFT JOIN" ++ RPerson.table ++ fr"p1 ON" ++ RItem.Columns.concPerson
|
||||||
|
.prefix("i")
|
||||||
|
.is(RPerson.Columns.pid.prefix("p1")) ++
|
||||||
|
fr"LEFT JOIN" ++ REquipment.table ++ fr"e ON" ++ RItem.Columns.concEquipment
|
||||||
|
.prefix("i")
|
||||||
|
.is(REquipment.Columns.eid.prefix("e")) ++
|
||||||
|
fr"LEFT JOIN" ++ RItem.table ++ fr"ref ON" ++ RItem.Columns.inReplyTo
|
||||||
|
.prefix("i")
|
||||||
|
.is(RItem.Columns.id.prefix("ref")) ++
|
||||||
fr"WHERE" ++ RItem.Columns.id.prefix("i").is(id)
|
fr"WHERE" ++ RItem.Columns.id.prefix("i").is(id)
|
||||||
|
|
||||||
val q = cq.query[(RItem, Option[ROrganization], Option[RPerson], Option[RPerson], Option[REquipment], Option[IdRef])].option
|
val q = cq
|
||||||
|
.query[
|
||||||
|
(
|
||||||
|
RItem,
|
||||||
|
Option[ROrganization],
|
||||||
|
Option[RPerson],
|
||||||
|
Option[RPerson],
|
||||||
|
Option[REquipment],
|
||||||
|
Option[IdRef]
|
||||||
|
)
|
||||||
|
]
|
||||||
|
.option
|
||||||
val attachs = RAttachment.findByItemWithMeta(id)
|
val attachs = RAttachment.findByItemWithMeta(id)
|
||||||
|
|
||||||
val tags = RTag.findByItem(id)
|
val tags = RTag.findByItem(id)
|
||||||
@ -56,35 +79,38 @@ object QItem {
|
|||||||
} yield data.map(d => ItemData(d._1, d._2, d._3, d._4, d._5, d._6, ts, att))
|
} yield data.map(d => ItemData(d._1, d._2, d._3, d._4, d._5, d._6, ts, att))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case class ListItem(
|
||||||
|
id: Ident,
|
||||||
|
name: String,
|
||||||
|
state: ItemState,
|
||||||
|
date: Timestamp,
|
||||||
|
dueDate: Option[Timestamp],
|
||||||
|
source: String,
|
||||||
|
direction: Direction,
|
||||||
|
created: Timestamp,
|
||||||
|
fileCount: Int,
|
||||||
|
corrOrg: Option[IdRef],
|
||||||
|
corrPerson: Option[IdRef],
|
||||||
|
concPerson: Option[IdRef],
|
||||||
|
concEquip: Option[IdRef]
|
||||||
|
)
|
||||||
|
|
||||||
case class ListItem( id: Ident
|
case class Query(
|
||||||
, name: String
|
collective: Ident,
|
||||||
, state: ItemState
|
name: Option[String],
|
||||||
, date: Timestamp
|
states: Seq[ItemState],
|
||||||
, dueDate: Option[Timestamp]
|
direction: Option[Direction],
|
||||||
, source: String
|
corrPerson: Option[Ident],
|
||||||
, direction: Direction
|
corrOrg: Option[Ident],
|
||||||
, created: Timestamp
|
concPerson: Option[Ident],
|
||||||
, fileCount: Int
|
concEquip: Option[Ident],
|
||||||
, corrOrg: Option[IdRef]
|
tagsInclude: List[Ident],
|
||||||
, corrPerson: Option[IdRef]
|
tagsExclude: List[Ident],
|
||||||
, concPerson: Option[IdRef]
|
dateFrom: Option[Timestamp],
|
||||||
, concEquip: Option[IdRef])
|
dateTo: Option[Timestamp],
|
||||||
|
dueDateFrom: Option[Timestamp],
|
||||||
case class Query( collective: Ident
|
dueDateTo: Option[Timestamp]
|
||||||
, name: Option[String]
|
)
|
||||||
, states: Seq[ItemState]
|
|
||||||
, direction: Option[Direction]
|
|
||||||
, corrPerson: Option[Ident]
|
|
||||||
, corrOrg: Option[Ident]
|
|
||||||
, concPerson: Option[Ident]
|
|
||||||
, concEquip: Option[Ident]
|
|
||||||
, tagsInclude: List[Ident]
|
|
||||||
, tagsExclude: List[Ident]
|
|
||||||
, dateFrom: Option[Timestamp]
|
|
||||||
, dateTo: Option[Timestamp]
|
|
||||||
, dueDateFrom: Option[Timestamp]
|
|
||||||
, dueDateTo: Option[Timestamp])
|
|
||||||
|
|
||||||
def findItems(q: Query): Stream[ConnectionIO, ListItem] = {
|
def findItems(q: Query): Stream[ConnectionIO, ListItem] = {
|
||||||
val IC = RItem.Columns
|
val IC = RItem.Columns
|
||||||
@ -97,70 +123,93 @@ object QItem {
|
|||||||
val orgCols = List(ROrganization.Columns.oid, ROrganization.Columns.name)
|
val orgCols = List(ROrganization.Columns.oid, ROrganization.Columns.name)
|
||||||
val equipCols = List(REquipment.Columns.eid, REquipment.Columns.name)
|
val equipCols = List(REquipment.Columns.eid, REquipment.Columns.name)
|
||||||
|
|
||||||
val finalCols = commas(IC.id.prefix("i").f
|
val finalCols = commas(
|
||||||
, IC.name.prefix("i").f
|
IC.id.prefix("i").f,
|
||||||
, IC.state.prefix("i").f
|
IC.name.prefix("i").f,
|
||||||
, coalesce(IC.itemDate.prefix("i").f, IC.created.prefix("i").f)
|
IC.state.prefix("i").f,
|
||||||
, IC.dueDate.prefix("i").f
|
coalesce(IC.itemDate.prefix("i").f, IC.created.prefix("i").f),
|
||||||
, IC.source.prefix("i").f
|
IC.dueDate.prefix("i").f,
|
||||||
, IC.incoming.prefix("i").f
|
IC.source.prefix("i").f,
|
||||||
, IC.created.prefix("i").f
|
IC.incoming.prefix("i").f,
|
||||||
, fr"COALESCE(a.num, 0)"
|
IC.created.prefix("i").f,
|
||||||
, OC.oid.prefix("o0").f
|
fr"COALESCE(a.num, 0)",
|
||||||
, OC.name.prefix("o0").f
|
OC.oid.prefix("o0").f,
|
||||||
, PC.pid.prefix("p0").f
|
OC.name.prefix("o0").f,
|
||||||
, PC.name.prefix("p0").f
|
PC.pid.prefix("p0").f,
|
||||||
, PC.pid.prefix("p1").f
|
PC.name.prefix("p0").f,
|
||||||
, PC.name.prefix("p1").f
|
PC.pid.prefix("p1").f,
|
||||||
, EC.eid.prefix("e1").f
|
PC.name.prefix("p1").f,
|
||||||
, EC.name.prefix("e1").f
|
EC.eid.prefix("e1").f,
|
||||||
|
EC.name.prefix("e1").f
|
||||||
)
|
)
|
||||||
|
|
||||||
val withItem = selectSimple(itemCols, RItem.table, IC.cid is q.collective)
|
val withItem = selectSimple(itemCols, RItem.table, IC.cid.is(q.collective))
|
||||||
val withPerson = selectSimple(personCols, RPerson.table, PC.cid is q.collective)
|
val withPerson = selectSimple(personCols, RPerson.table, PC.cid.is(q.collective))
|
||||||
val withOrgs = selectSimple(orgCols, ROrganization.table, OC.cid is q.collective)
|
val withOrgs = selectSimple(orgCols, ROrganization.table, OC.cid.is(q.collective))
|
||||||
val withEquips = selectSimple(equipCols, REquipment.table, EC.cid is q.collective)
|
val withEquips = selectSimple(equipCols, REquipment.table, EC.cid.is(q.collective))
|
||||||
val withAttach = fr"SELECT COUNT(" ++ AC.id.f ++ fr") as num, " ++ AC.itemId.f ++
|
val withAttach = fr"SELECT COUNT(" ++ AC.id.f ++ fr") as num, " ++ AC.itemId.f ++
|
||||||
fr"from" ++ RAttachment.table ++ fr"GROUP BY (" ++ AC.itemId.f ++ fr")"
|
fr"from" ++ RAttachment.table ++ fr"GROUP BY (" ++ AC.itemId.f ++ fr")"
|
||||||
|
|
||||||
val query = withCTE("items" -> withItem
|
val query = withCTE(
|
||||||
, "persons" -> withPerson
|
"items" -> withItem,
|
||||||
, "orgs" -> withOrgs
|
"persons" -> withPerson,
|
||||||
, "equips" -> withEquips
|
"orgs" -> withOrgs,
|
||||||
, "attachs" -> withAttach) ++
|
"equips" -> withEquips,
|
||||||
|
"attachs" -> withAttach
|
||||||
|
) ++
|
||||||
fr"SELECT DISTINCT" ++ finalCols ++ fr" FROM items i" ++
|
fr"SELECT DISTINCT" ++ finalCols ++ fr" FROM items i" ++
|
||||||
fr"LEFT JOIN attachs a ON" ++ IC.id.prefix("i").is(AC.itemId.prefix("a")) ++
|
fr"LEFT JOIN attachs a ON" ++ IC.id.prefix("i").is(AC.itemId.prefix("a")) ++
|
||||||
fr"LEFT JOIN persons p0 ON" ++ IC.corrPerson.prefix("i").is(PC.pid.prefix("p0")) ++ // i.corrperson = p0.pid" ++
|
fr"LEFT JOIN persons p0 ON" ++ IC.corrPerson
|
||||||
fr"LEFT JOIN orgs o0 ON" ++ IC.corrOrg.prefix("i").is(OC.oid.prefix("o0")) ++ // i.corrorg = o0.oid" ++
|
.prefix("i")
|
||||||
fr"LEFT JOIN persons p1 ON" ++ IC.concPerson.prefix("i").is(PC.pid.prefix("p1")) ++ // i.concperson = p1.pid" ++
|
.is(PC.pid.prefix("p0")) ++ // i.corrperson = p0.pid" ++
|
||||||
fr"LEFT JOIN equips e1 ON" ++ IC.concEquipment.prefix("i").is(EC.eid.prefix("e1")) // i.concequipment = e1.eid"
|
fr"LEFT JOIN orgs o0 ON" ++ IC.corrOrg
|
||||||
|
.prefix("i")
|
||||||
|
.is(OC.oid.prefix("o0")) ++ // i.corrorg = o0.oid" ++
|
||||||
|
fr"LEFT JOIN persons p1 ON" ++ IC.concPerson
|
||||||
|
.prefix("i")
|
||||||
|
.is(PC.pid.prefix("p1")) ++ // i.concperson = p1.pid" ++
|
||||||
|
fr"LEFT JOIN equips e1 ON" ++ IC.concEquipment
|
||||||
|
.prefix("i")
|
||||||
|
.is(EC.eid.prefix("e1")) // i.concequipment = e1.eid"
|
||||||
|
|
||||||
// inclusive tags are AND-ed
|
// inclusive tags are AND-ed
|
||||||
val tagSelectsIncl = q.tagsInclude.map(tid =>
|
val tagSelectsIncl = q.tagsInclude
|
||||||
selectSimple(List(RTagItem.Columns.itemId), RTagItem.table, RTagItem.Columns.tagId is tid)).
|
.map(tid =>
|
||||||
map(f => sql"(" ++ f ++ sql") ")
|
selectSimple(List(RTagItem.Columns.itemId), RTagItem.table, RTagItem.Columns.tagId.is(tid))
|
||||||
|
)
|
||||||
|
.map(f => sql"(" ++ f ++ sql") ")
|
||||||
|
|
||||||
// exclusive tags are OR-ed
|
// exclusive tags are OR-ed
|
||||||
val tagSelectsExcl =
|
val tagSelectsExcl =
|
||||||
if (q.tagsExclude.isEmpty) Fragment.empty
|
if (q.tagsExclude.isEmpty) Fragment.empty
|
||||||
else selectSimple(List(RTagItem.Columns.itemId), RTagItem.table, RTagItem.Columns.tagId isOneOf q.tagsExclude)
|
else
|
||||||
|
selectSimple(
|
||||||
|
List(RTagItem.Columns.itemId),
|
||||||
|
RTagItem.table,
|
||||||
|
RTagItem.Columns.tagId.isOneOf(q.tagsExclude)
|
||||||
|
)
|
||||||
|
|
||||||
val name = q.name.map(queryWildcard)
|
val name = q.name.map(queryWildcard)
|
||||||
val cond = and(
|
val cond = and(
|
||||||
IC.cid.prefix("i") is q.collective,
|
IC.cid.prefix("i").is(q.collective),
|
||||||
IC.state.prefix("i") isOneOf q.states,
|
IC.state.prefix("i").isOneOf(q.states),
|
||||||
IC.incoming.prefix("i") isOrDiscard q.direction,
|
IC.incoming.prefix("i").isOrDiscard(q.direction),
|
||||||
name.map(n => IC.name.prefix("i").lowerLike(n)).getOrElse(Fragment.empty),
|
name.map(n => IC.name.prefix("i").lowerLike(n)).getOrElse(Fragment.empty),
|
||||||
RPerson.Columns.pid.prefix("p0") isOrDiscard q.corrPerson,
|
RPerson.Columns.pid.prefix("p0").isOrDiscard(q.corrPerson),
|
||||||
ROrganization.Columns.oid.prefix("o0") isOrDiscard q.corrOrg,
|
ROrganization.Columns.oid.prefix("o0").isOrDiscard(q.corrOrg),
|
||||||
RPerson.Columns.pid.prefix("p1") isOrDiscard q.concPerson,
|
RPerson.Columns.pid.prefix("p1").isOrDiscard(q.concPerson),
|
||||||
REquipment.Columns.eid.prefix("e1") isOrDiscard q.concEquip,
|
REquipment.Columns.eid.prefix("e1").isOrDiscard(q.concEquip),
|
||||||
if (q.tagsInclude.isEmpty) Fragment.empty
|
if (q.tagsInclude.isEmpty) Fragment.empty
|
||||||
else IC.id.prefix("i") ++ sql" IN (" ++ tagSelectsIncl.reduce(_ ++ fr"INTERSECT" ++ _) ++ sql")",
|
else
|
||||||
|
IC.id.prefix("i") ++ sql" IN (" ++ tagSelectsIncl.reduce(_ ++ fr"INTERSECT" ++ _) ++ sql")",
|
||||||
if (q.tagsExclude.isEmpty) Fragment.empty
|
if (q.tagsExclude.isEmpty) Fragment.empty
|
||||||
else IC.id.prefix("i").f ++ sql" NOT IN (" ++ tagSelectsExcl ++ sql")",
|
else IC.id.prefix("i").f ++ sql" NOT IN (" ++ tagSelectsExcl ++ sql")",
|
||||||
q.dateFrom.map(d => coalesce(IC.itemDate.prefix("i").f, IC.created.prefix("i").f) ++ fr">= $d").getOrElse(Fragment.empty),
|
q.dateFrom
|
||||||
q.dateTo.map(d => coalesce(IC.itemDate.prefix("i").f, IC.created.prefix("i").f) ++ fr"<= $d").getOrElse(Fragment.empty),
|
.map(d => coalesce(IC.itemDate.prefix("i").f, IC.created.prefix("i").f) ++ fr">= $d")
|
||||||
|
.getOrElse(Fragment.empty),
|
||||||
|
q.dateTo
|
||||||
|
.map(d => coalesce(IC.itemDate.prefix("i").f, IC.created.prefix("i").f) ++ fr"<= $d")
|
||||||
|
.getOrElse(Fragment.empty),
|
||||||
q.dueDateFrom.map(d => IC.dueDate.prefix("i").isGt(d)).getOrElse(Fragment.empty),
|
q.dueDateFrom.map(d => IC.dueDate.prefix("i").isGt(d)).getOrElse(Fragment.empty),
|
||||||
q.dueDateTo.map(d => IC.dueDate.prefix("i").isLt(d)).getOrElse(Fragment.empty)
|
q.dueDateTo.map(d => IC.dueDate.prefix("i").isLt(d)).getOrElse(Fragment.empty)
|
||||||
)
|
)
|
||||||
@ -171,7 +220,6 @@ object QItem {
|
|||||||
frag.query[ListItem].stream
|
frag.query[ListItem].stream
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def delete[F[_]: Sync](store: Store[F])(itemId: Ident, collective: Ident): F[Int] =
|
def delete[F[_]: Sync](store: Store[F])(itemId: Ident, collective: Ident): F[Int] =
|
||||||
for {
|
for {
|
||||||
tn <- store.transact(RTagItem.deleteItemTags(itemId))
|
tn <- store.transact(RTagItem.deleteItemTags(itemId))
|
||||||
@ -183,7 +231,9 @@ object QItem {
|
|||||||
val IC = RItem.Columns
|
val IC = RItem.Columns
|
||||||
val AC = RAttachment.Columns
|
val AC = RAttachment.Columns
|
||||||
val q = fr"SELECT DISTINCT" ++ commas(IC.all.map(_.prefix("i").f)) ++ fr"FROM" ++ RItem.table ++ fr"i" ++
|
val q = fr"SELECT DISTINCT" ++ commas(IC.all.map(_.prefix("i").f)) ++ fr"FROM" ++ RItem.table ++ fr"i" ++
|
||||||
fr"INNER JOIN" ++ RAttachment.table ++ fr"a ON" ++ AC.itemId.prefix("a").is(IC.id.prefix("i")) ++
|
fr"INNER JOIN" ++ RAttachment.table ++ fr"a ON" ++ AC.itemId
|
||||||
|
.prefix("a")
|
||||||
|
.is(IC.id.prefix("i")) ++
|
||||||
fr"WHERE" ++ AC.fileId.isOneOf(fileMetaIds) ++ orderBy(IC.created.prefix("i").asc)
|
fr"WHERE" ++ AC.fileId.isOneOf(fileMetaIds) ++ orderBy(IC.created.prefix("i").asc)
|
||||||
|
|
||||||
q.query[RItem].to[Vector]
|
q.query[RItem].to[Vector]
|
||||||
|
@ -13,29 +13,38 @@ import fs2.Stream
|
|||||||
import org.log4s._
|
import org.log4s._
|
||||||
|
|
||||||
object QJob {
|
object QJob {
|
||||||
private [this] val logger = getLogger
|
private[this] val logger = getLogger
|
||||||
|
|
||||||
def takeNextJob[F[_]: Effect](store: Store[F])(priority: Ident => F[Priority], worker: Ident, retryPause: Duration): F[Option[RJob]] = {
|
def takeNextJob[F[_]: Effect](
|
||||||
Stream.range(0, 10).
|
store: Store[F]
|
||||||
evalMap(n => takeNextJob1(store)(priority, worker, retryPause, n)).
|
)(priority: Ident => F[Priority], worker: Ident, retryPause: Duration): F[Option[RJob]] =
|
||||||
evalTap({ x =>
|
Stream
|
||||||
if (x.isLeft) logger.fdebug[F]("Cannot mark job, probably due to concurrent updates. Will retry.")
|
.range(0, 10)
|
||||||
|
.evalMap(n => takeNextJob1(store)(priority, worker, retryPause, n))
|
||||||
|
.evalTap({ x =>
|
||||||
|
if (x.isLeft)
|
||||||
|
logger.fdebug[F]("Cannot mark job, probably due to concurrent updates. Will retry.")
|
||||||
else ().pure[F]
|
else ().pure[F]
|
||||||
}).
|
})
|
||||||
find(_.isRight).
|
.find(_.isRight)
|
||||||
flatMap({
|
.flatMap({
|
||||||
case Right(job) =>
|
case Right(job) =>
|
||||||
Stream.emit(job)
|
Stream.emit(job)
|
||||||
case Left(_) =>
|
case Left(_) =>
|
||||||
Stream.eval(logger.fwarn[F]("Cannot mark job, even after retrying. Give up.")).map(_ => None)
|
Stream
|
||||||
}).
|
.eval(logger.fwarn[F]("Cannot mark job, even after retrying. Give up."))
|
||||||
compile.last.map(_.flatten)
|
.map(_ => None)
|
||||||
}
|
})
|
||||||
|
.compile
|
||||||
|
.last
|
||||||
|
.map(_.flatten)
|
||||||
|
|
||||||
private def takeNextJob1[F[_]: Effect](store: Store[F])( priority: Ident => F[Priority]
|
private def takeNextJob1[F[_]: Effect](store: Store[F])(
|
||||||
, worker: Ident
|
priority: Ident => F[Priority],
|
||||||
, retryPause: Duration
|
worker: Ident,
|
||||||
, currentTry: Int): F[Either[Unit, Option[RJob]]] = {
|
retryPause: Duration,
|
||||||
|
currentTry: Int
|
||||||
|
): F[Either[Unit, Option[RJob]]] = {
|
||||||
//if this fails, we have to restart takeNextJob
|
//if this fails, we have to restart takeNextJob
|
||||||
def markJob(job: RJob): F[Either[Unit, RJob]] =
|
def markJob(job: RJob): F[Either[Unit, RJob]] =
|
||||||
store.transact(for {
|
store.transact(for {
|
||||||
@ -51,7 +60,9 @@ object QJob {
|
|||||||
_ <- logger.ftrace[F](s"Choose group ${group.map(_.id)}")
|
_ <- logger.ftrace[F](s"Choose group ${group.map(_.id)}")
|
||||||
prio <- group.map(priority).getOrElse((Priority.Low: Priority).pure[F])
|
prio <- group.map(priority).getOrElse((Priority.Low: Priority).pure[F])
|
||||||
_ <- logger.ftrace[F](s"Looking for job of prio $prio")
|
_ <- logger.ftrace[F](s"Looking for job of prio $prio")
|
||||||
job <- group.map(g => store.transact(selectNextJob(g, prio, retryPause, now))).getOrElse((None: Option[RJob]).pure[F])
|
job <- group
|
||||||
|
.map(g => store.transact(selectNextJob(g, prio, retryPause, now)))
|
||||||
|
.getOrElse((None: Option[RJob]).pure[F])
|
||||||
_ <- logger.ftrace[F](s"Found job: ${job.map(_.info)}")
|
_ <- logger.ftrace[F](s"Found job: ${job.map(_.info)}")
|
||||||
res <- job.traverse(j => markJob(j))
|
res <- job.traverse(j => markJob(j))
|
||||||
} yield res.map(_.map(_.some)).getOrElse {
|
} yield res.map(_.map(_.some)).getOrElse {
|
||||||
@ -60,7 +71,11 @@ object QJob {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
def selectNextGroup(worker: Ident, now: Timestamp, initialPause: Duration): ConnectionIO[Option[Ident]] = {
|
def selectNextGroup(
|
||||||
|
worker: Ident,
|
||||||
|
now: Timestamp,
|
||||||
|
initialPause: Duration
|
||||||
|
): ConnectionIO[Option[Ident]] = {
|
||||||
val JC = RJob.Columns
|
val JC = RJob.Columns
|
||||||
val waiting: JobState = JobState.Waiting
|
val waiting: JobState = JobState.Waiting
|
||||||
val stuck: JobState = JobState.Stuck
|
val stuck: JobState = JobState.Stuck
|
||||||
@ -72,21 +87,30 @@ object QJob {
|
|||||||
val stuckTrigger = coalesce(JC.startedmillis.prefix("a").f, sql"${now.toMillis}") ++
|
val stuckTrigger = coalesce(JC.startedmillis.prefix("a").f, sql"${now.toMillis}") ++
|
||||||
fr"+" ++ power2(JC.retries.prefix("a")) ++ fr"* ${initialPause.millis}"
|
fr"+" ++ power2(JC.retries.prefix("a")) ++ fr"* ${initialPause.millis}"
|
||||||
|
|
||||||
val stateCond = or(jstate is waiting, and(jstate is stuck, stuckTrigger ++ fr"< ${now.toMillis}"))
|
val stateCond =
|
||||||
|
or(jstate.is(waiting), and(jstate.is(stuck), stuckTrigger ++ fr"< ${now.toMillis}"))
|
||||||
|
|
||||||
val sql1 = fr"SELECT" ++ jgroup.f ++ fr"as g FROM" ++ RJob.table ++ fr"a" ++
|
val sql1 = fr"SELECT" ++ jgroup.f ++ fr"as g FROM" ++ RJob.table ++ fr"a" ++
|
||||||
fr"INNER JOIN" ++ RJobGroupUse.table ++ fr"b ON" ++ jgroup.isGt(ugroup) ++
|
fr"INNER JOIN" ++ RJobGroupUse.table ++ fr"b ON" ++ jgroup.isGt(ugroup) ++
|
||||||
fr"WHERE" ++ and(uworker is worker, stateCond) ++
|
fr"WHERE" ++ and(uworker.is(worker), stateCond) ++
|
||||||
fr"LIMIT 1" //LIMIT is not sql standard, but supported by h2,mariadb and postgres
|
fr"LIMIT 1" //LIMIT is not sql standard, but supported by h2,mariadb and postgres
|
||||||
val sql2 = fr"SELECT min(" ++ jgroup.f ++ fr") as g FROM" ++ RJob.table ++ fr"a" ++
|
val sql2 = fr"SELECT min(" ++ jgroup.f ++ fr") as g FROM" ++ RJob.table ++ fr"a" ++
|
||||||
fr"WHERE" ++ stateCond
|
fr"WHERE" ++ stateCond
|
||||||
|
|
||||||
val union = sql"SELECT g FROM ((" ++ sql1 ++ sql") UNION ALL (" ++ sql2 ++ sql")) as t0 WHERE g is not null"
|
val union = sql"SELECT g FROM ((" ++ sql1 ++ sql") UNION ALL (" ++ sql2 ++ sql")) as t0 WHERE g is not null"
|
||||||
|
|
||||||
union.query[Ident].to[List].map(_.headOption) // either one or two results, but may be empty if RJob table is empty
|
union
|
||||||
|
.query[Ident]
|
||||||
|
.to[List]
|
||||||
|
.map(_.headOption) // either one or two results, but may be empty if RJob table is empty
|
||||||
}
|
}
|
||||||
|
|
||||||
def selectNextJob(group: Ident, prio: Priority, initialPause: Duration, now: Timestamp): ConnectionIO[Option[RJob]] = {
|
def selectNextJob(
|
||||||
|
group: Ident,
|
||||||
|
prio: Priority,
|
||||||
|
initialPause: Duration,
|
||||||
|
now: Timestamp
|
||||||
|
): ConnectionIO[Option[RJob]] = {
|
||||||
val JC = RJob.Columns
|
val JC = RJob.Columns
|
||||||
val psort =
|
val psort =
|
||||||
if (prio == Priority.High) JC.priority.desc
|
if (prio == Priority.High) JC.priority.desc
|
||||||
@ -94,9 +118,17 @@ object QJob {
|
|||||||
val waiting: JobState = JobState.Waiting
|
val waiting: JobState = JobState.Waiting
|
||||||
val stuck: JobState = JobState.Stuck
|
val stuck: JobState = JobState.Stuck
|
||||||
|
|
||||||
val stuckTrigger = coalesce(JC.startedmillis.f, sql"${now.toMillis}") ++ fr"+" ++ power2(JC.retries) ++ fr"* ${initialPause.millis}"
|
val stuckTrigger = coalesce(JC.startedmillis.f, sql"${now.toMillis}") ++ fr"+" ++ power2(
|
||||||
val sql = selectSimple(JC.all, RJob.table,
|
JC.retries
|
||||||
and(JC.group is group, or(JC.state is waiting, and(JC.state is stuck, stuckTrigger ++ fr"< ${now.toMillis}")))) ++
|
) ++ fr"* ${initialPause.millis}"
|
||||||
|
val sql = selectSimple(
|
||||||
|
JC.all,
|
||||||
|
RJob.table,
|
||||||
|
and(
|
||||||
|
JC.group.is(group),
|
||||||
|
or(JC.state.is(waiting), and(JC.state.is(stuck), stuckTrigger ++ fr"< ${now.toMillis}"))
|
||||||
|
)
|
||||||
|
) ++
|
||||||
orderBy(JC.state.asc, psort, JC.submitted.asc) ++
|
orderBy(JC.state.asc, psort, JC.submitted.asc) ++
|
||||||
fr"LIMIT 1"
|
fr"LIMIT 1"
|
||||||
|
|
||||||
@ -150,9 +182,8 @@ object QJob {
|
|||||||
def exceedsRetries[F[_]: Effect](id: Ident, max: Int, store: Store[F]): F[Boolean] =
|
def exceedsRetries[F[_]: Effect](id: Ident, max: Int, store: Store[F]): F[Boolean] =
|
||||||
store.transact(RJob.getRetries(id)).map(n => n.forall(_ >= max))
|
store.transact(RJob.getRetries(id)).map(n => n.forall(_ >= max))
|
||||||
|
|
||||||
def runningToWaiting[F[_]: Effect](workerId: Ident, store: Store[F]): F[Unit] = {
|
def runningToWaiting[F[_]: Effect](workerId: Ident, store: Store[F]): F[Unit] =
|
||||||
store.transact(RJob.setRunningToWaiting(workerId)).map(_ => ())
|
store.transact(RJob.setRunningToWaiting(workerId)).map(_ => ())
|
||||||
}
|
|
||||||
|
|
||||||
def findAll[F[_]: Effect](ids: Seq[Ident], store: Store[F]): F[Vector[RJob]] =
|
def findAll[F[_]: Effect](ids: Seq[Ident], store: Store[F]): F[Vector[RJob]] =
|
||||||
store.transact(RJob.findFromIds(ids))
|
store.transact(RJob.findFromIds(ids))
|
||||||
@ -165,10 +196,17 @@ object QJob {
|
|||||||
|
|
||||||
def selectJobs(now: Timestamp): Stream[ConnectionIO, RJob] = {
|
def selectJobs(now: Timestamp): Stream[ConnectionIO, RJob] = {
|
||||||
val refDate = now.minusHours(24)
|
val refDate = now.minusHours(24)
|
||||||
val sql = selectSimple(JC.all, RJob.table,
|
val sql = selectSimple(
|
||||||
and(JC.group is collective,
|
JC.all,
|
||||||
or(and(JC.state.isOneOf(done.toSeq), JC.submitted isGt refDate)
|
RJob.table,
|
||||||
, JC.state.isOneOf((running ++ waiting).toSeq))))
|
and(
|
||||||
|
JC.group.is(collective),
|
||||||
|
or(
|
||||||
|
and(JC.state.isOneOf(done.toSeq), JC.submitted.isGt(refDate)),
|
||||||
|
JC.state.isOneOf((running ++ waiting).toSeq)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
(sql ++ orderBy(JC.submitted.desc)).query[RJob].stream
|
(sql ++ orderBy(JC.submitted.desc)).query[RJob].stream
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -12,10 +12,12 @@ import org.log4s._
|
|||||||
object QLogin {
|
object QLogin {
|
||||||
private[this] val logger = getLogger
|
private[this] val logger = getLogger
|
||||||
|
|
||||||
case class Data( account: AccountId
|
case class Data(
|
||||||
, password: Password
|
account: AccountId,
|
||||||
, collectiveState: CollectiveState
|
password: Password,
|
||||||
, userState: UserState)
|
collectiveState: CollectiveState,
|
||||||
|
userState: UserState
|
||||||
|
)
|
||||||
|
|
||||||
def findUser(acc: AccountId): ConnectionIO[Option[Data]] = {
|
def findUser(acc: AccountId): ConnectionIO[Option[Data]] = {
|
||||||
val ucid = UC.cid.prefix("u")
|
val ucid = UC.cid.prefix("u")
|
||||||
@ -26,9 +28,10 @@ object QLogin {
|
|||||||
val ccid = CC.id.prefix("c")
|
val ccid = CC.id.prefix("c")
|
||||||
|
|
||||||
val sql = selectSimple(
|
val sql = selectSimple(
|
||||||
List(ucid,login,pass,cstate,ustate),
|
List(ucid, login, pass, cstate, ustate),
|
||||||
RUser.table ++ fr"u, " ++ RCollective.table ++ fr"c",
|
RUser.table ++ fr"u, " ++ RCollective.table ++ fr"c",
|
||||||
and(ucid is ccid, login is acc.user, ucid is acc.collective))
|
and(ucid.is(ccid), login.is(acc.user), ucid.is(acc.collective))
|
||||||
|
)
|
||||||
|
|
||||||
logger.trace(s"SQL : $sql")
|
logger.trace(s"SQL : $sql")
|
||||||
sql.query[Data].option
|
sql.query[Data].option
|
||||||
|
@ -12,16 +12,24 @@ import docspell.store.records._
|
|||||||
|
|
||||||
object QOrganization {
|
object QOrganization {
|
||||||
|
|
||||||
def findOrgAndContact(coll: Ident, order: OC.type => Column): Stream[ConnectionIO, (ROrganization, Vector[RContact])] = {
|
def findOrgAndContact(
|
||||||
ROrganization.findAll(coll, order).
|
coll: Ident,
|
||||||
evalMap(ro => RContact.findAllOrg(ro.oid).map(cs => (ro, cs)))
|
order: OC.type => Column
|
||||||
}
|
): Stream[ConnectionIO, (ROrganization, Vector[RContact])] =
|
||||||
def findPersonAndContact(coll: Ident, order: PC.type => Column): Stream[ConnectionIO, (RPerson, Vector[RContact])] = {
|
ROrganization
|
||||||
RPerson.findAll(coll, order).
|
.findAll(coll, order)
|
||||||
evalMap(ro => RContact.findAllPerson(ro.pid).map(cs => (ro, cs)))
|
.evalMap(ro => RContact.findAllOrg(ro.oid).map(cs => (ro, cs)))
|
||||||
}
|
def findPersonAndContact(
|
||||||
|
coll: Ident,
|
||||||
|
order: PC.type => Column
|
||||||
|
): Stream[ConnectionIO, (RPerson, Vector[RContact])] =
|
||||||
|
RPerson.findAll(coll, order).evalMap(ro => RContact.findAllPerson(ro.pid).map(cs => (ro, cs)))
|
||||||
|
|
||||||
def addOrg[F[_]](org: ROrganization, contacts: Seq[RContact], cid: Ident): Store[F] => F[AddResult] = {
|
def addOrg[F[_]](
|
||||||
|
org: ROrganization,
|
||||||
|
contacts: Seq[RContact],
|
||||||
|
cid: Ident
|
||||||
|
): Store[F] => F[AddResult] = {
|
||||||
val insert = for {
|
val insert = for {
|
||||||
n <- ROrganization.insert(org)
|
n <- ROrganization.insert(org)
|
||||||
cs <- contacts.toList.traverse(RContact.insert)
|
cs <- contacts.toList.traverse(RContact.insert)
|
||||||
@ -32,7 +40,11 @@ object QOrganization {
|
|||||||
store => store.add(insert, exists)
|
store => store.add(insert, exists)
|
||||||
}
|
}
|
||||||
|
|
||||||
def addPerson[F[_]](person: RPerson, contacts: Seq[RContact], cid: Ident): Store[F] => F[AddResult] = {
|
def addPerson[F[_]](
|
||||||
|
person: RPerson,
|
||||||
|
contacts: Seq[RContact],
|
||||||
|
cid: Ident
|
||||||
|
): Store[F] => F[AddResult] = {
|
||||||
val insert = for {
|
val insert = for {
|
||||||
n <- RPerson.insert(person)
|
n <- RPerson.insert(person)
|
||||||
cs <- contacts.toList.traverse(RContact.insert)
|
cs <- contacts.toList.traverse(RContact.insert)
|
||||||
@ -43,7 +55,11 @@ object QOrganization {
|
|||||||
store => store.add(insert, exists)
|
store => store.add(insert, exists)
|
||||||
}
|
}
|
||||||
|
|
||||||
def updateOrg[F[_]](org: ROrganization, contacts: Seq[RContact], cid: Ident): Store[F] => F[AddResult] = {
|
def updateOrg[F[_]](
|
||||||
|
org: ROrganization,
|
||||||
|
contacts: Seq[RContact],
|
||||||
|
cid: Ident
|
||||||
|
): Store[F] => F[AddResult] = {
|
||||||
val insert = for {
|
val insert = for {
|
||||||
n <- ROrganization.update(org)
|
n <- ROrganization.update(org)
|
||||||
d <- RContact.deleteOrg(org.oid)
|
d <- RContact.deleteOrg(org.oid)
|
||||||
@ -55,7 +71,11 @@ object QOrganization {
|
|||||||
store => store.add(insert, exists)
|
store => store.add(insert, exists)
|
||||||
}
|
}
|
||||||
|
|
||||||
def updatePerson[F[_]](person: RPerson, contacts: Seq[RContact], cid: Ident): Store[F] => F[AddResult] = {
|
def updatePerson[F[_]](
|
||||||
|
person: RPerson,
|
||||||
|
contacts: Seq[RContact],
|
||||||
|
cid: Ident
|
||||||
|
): Store[F] => F[AddResult] = {
|
||||||
val insert = for {
|
val insert = for {
|
||||||
n <- RPerson.update(person)
|
n <- RPerson.update(person)
|
||||||
d <- RContact.deletePerson(person.pid)
|
d <- RContact.deletePerson(person.pid)
|
||||||
@ -67,20 +87,18 @@ object QOrganization {
|
|||||||
store => store.add(insert, exists)
|
store => store.add(insert, exists)
|
||||||
}
|
}
|
||||||
|
|
||||||
def deleteOrg(orgId: Ident, collective: Ident): ConnectionIO[Int] = {
|
def deleteOrg(orgId: Ident, collective: Ident): ConnectionIO[Int] =
|
||||||
for {
|
for {
|
||||||
n0 <- RItem.removeCorrOrg(collective, orgId)
|
n0 <- RItem.removeCorrOrg(collective, orgId)
|
||||||
n1 <- RContact.deleteOrg(orgId)
|
n1 <- RContact.deleteOrg(orgId)
|
||||||
n2 <- ROrganization.delete(orgId, collective)
|
n2 <- ROrganization.delete(orgId, collective)
|
||||||
} yield n0 + n1 + n2
|
} yield n0 + n1 + n2
|
||||||
}
|
|
||||||
|
|
||||||
def deletePerson(personId: Ident, collective: Ident): ConnectionIO[Int] = {
|
def deletePerson(personId: Ident, collective: Ident): ConnectionIO[Int] =
|
||||||
for {
|
for {
|
||||||
n0 <- RItem.removeCorrPerson(collective, personId)
|
n0 <- RItem.removeCorrPerson(collective, personId)
|
||||||
n1 <- RItem.removeConcPerson(collective, personId)
|
n1 <- RItem.removeConcPerson(collective, personId)
|
||||||
n2 <- RContact.deletePerson(personId)
|
n2 <- RContact.deletePerson(personId)
|
||||||
n3 <- RPerson.delete(personId, collective)
|
n3 <- RPerson.delete(personId, collective)
|
||||||
} yield n0 + n1 + n2 + n3
|
} yield n0 + n1 + n2 + n3
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -21,22 +21,29 @@ trait JobQueue[F[_]] {
|
|||||||
object JobQueue {
|
object JobQueue {
|
||||||
private[this] val logger = getLogger
|
private[this] val logger = getLogger
|
||||||
|
|
||||||
def apply[F[_] : Effect](store: Store[F]): Resource[F, JobQueue[F]] =
|
def apply[F[_]: Effect](store: Store[F]): Resource[F, JobQueue[F]] =
|
||||||
Resource.pure(new JobQueue[F] {
|
Resource.pure(new JobQueue[F] {
|
||||||
|
|
||||||
def nextJob(prio: Ident => F[Priority], worker: Ident, retryPause: Duration): F[Option[RJob]] =
|
def nextJob(
|
||||||
|
prio: Ident => F[Priority],
|
||||||
|
worker: Ident,
|
||||||
|
retryPause: Duration
|
||||||
|
): F[Option[RJob]] =
|
||||||
logger.fdebug("Select next job") *> QJob.takeNextJob(store)(prio, worker, retryPause)
|
logger.fdebug("Select next job") *> QJob.takeNextJob(store)(prio, worker, retryPause)
|
||||||
|
|
||||||
def insert(job: RJob): F[Unit] =
|
def insert(job: RJob): F[Unit] =
|
||||||
store.transact(RJob.insert(job)).
|
store
|
||||||
flatMap({ n =>
|
.transact(RJob.insert(job))
|
||||||
if (n != 1) Effect[F].raiseError(new Exception(s"Inserting job failed. Update count: $n"))
|
.flatMap({ n =>
|
||||||
|
if (n != 1)
|
||||||
|
Effect[F].raiseError(new Exception(s"Inserting job failed. Update count: $n"))
|
||||||
else ().pure[F]
|
else ().pure[F]
|
||||||
})
|
})
|
||||||
|
|
||||||
def insertAll(jobs: Seq[RJob]): F[Unit] =
|
def insertAll(jobs: Seq[RJob]): F[Unit] =
|
||||||
jobs.toList.traverse(j => insert(j).attempt).
|
jobs.toList
|
||||||
map(_.foreach {
|
.traverse(j => insert(j).attempt)
|
||||||
|
.map(_.foreach {
|
||||||
case Right(()) =>
|
case Right(()) =>
|
||||||
case Left(ex) =>
|
case Left(ex) =>
|
||||||
logger.error(ex)("Could not insert job. Skipping it.")
|
logger.error(ex)("Could not insert job. Skipping it.")
|
||||||
|
@ -7,14 +7,14 @@ import docspell.common._
|
|||||||
import docspell.store.impl._
|
import docspell.store.impl._
|
||||||
import docspell.store.impl.Implicits._
|
import docspell.store.impl.Implicits._
|
||||||
|
|
||||||
case class RAttachment( id: Ident
|
case class RAttachment(
|
||||||
, itemId: Ident
|
id: Ident,
|
||||||
, fileId: Ident
|
itemId: Ident,
|
||||||
, position: Int
|
fileId: Ident,
|
||||||
, created: Timestamp
|
position: Int,
|
||||||
, name: Option[String]) {
|
created: Timestamp,
|
||||||
|
name: Option[String]
|
||||||
}
|
) {}
|
||||||
|
|
||||||
object RAttachment {
|
object RAttachment {
|
||||||
|
|
||||||
@ -32,25 +32,34 @@ object RAttachment {
|
|||||||
import Columns._
|
import Columns._
|
||||||
|
|
||||||
def insert(v: RAttachment): ConnectionIO[Int] =
|
def insert(v: RAttachment): ConnectionIO[Int] =
|
||||||
insertRow(table, all, fr"${v.id},${v.itemId},${v.fileId.id},${v.position},${v.created},${v.name}").update.run
|
insertRow(
|
||||||
|
table,
|
||||||
|
all,
|
||||||
|
fr"${v.id},${v.itemId},${v.fileId.id},${v.position},${v.created},${v.name}"
|
||||||
|
).update.run
|
||||||
|
|
||||||
def findById(attachId: Ident): ConnectionIO[Option[RAttachment]] =
|
def findById(attachId: Ident): ConnectionIO[Option[RAttachment]] =
|
||||||
selectSimple(all, table, id is attachId).query[RAttachment].option
|
selectSimple(all, table, id.is(attachId)).query[RAttachment].option
|
||||||
|
|
||||||
def findByIdAndCollective(attachId: Ident, collective: Ident): ConnectionIO[Option[RAttachment]] = {
|
def findByIdAndCollective(attachId: Ident, collective: Ident): ConnectionIO[Option[RAttachment]] =
|
||||||
selectSimple(all.map(_.prefix("a")), table ++ fr"a," ++ RItem.table ++ fr"i", and(
|
selectSimple(
|
||||||
|
all.map(_.prefix("a")),
|
||||||
|
table ++ fr"a," ++ RItem.table ++ fr"i",
|
||||||
|
and(
|
||||||
fr"a.itemid = i.itemid",
|
fr"a.itemid = i.itemid",
|
||||||
id.prefix("a") is attachId,
|
id.prefix("a").is(attachId),
|
||||||
RItem.Columns.cid.prefix("i") is collective
|
RItem.Columns.cid.prefix("i").is(collective)
|
||||||
)).query[RAttachment].option
|
)
|
||||||
}
|
).query[RAttachment].option
|
||||||
|
|
||||||
def findByItem(id: Ident): ConnectionIO[Vector[RAttachment]] =
|
def findByItem(id: Ident): ConnectionIO[Vector[RAttachment]] =
|
||||||
selectSimple(all, table, itemId is id).query[RAttachment].to[Vector]
|
selectSimple(all, table, itemId.is(id)).query[RAttachment].to[Vector]
|
||||||
|
|
||||||
def findByItemAndCollective(id: Ident, coll: Ident): ConnectionIO[Vector[RAttachment]] = {
|
def findByItemAndCollective(id: Ident, coll: Ident): ConnectionIO[Vector[RAttachment]] = {
|
||||||
val q = selectSimple(all.map(_.prefix("a")), table ++ fr"a", Fragment.empty) ++
|
val q = selectSimple(all.map(_.prefix("a")), table ++ fr"a", Fragment.empty) ++
|
||||||
fr"INNER JOIN" ++ RItem.table ++ fr"i ON" ++ RItem.Columns.id.prefix("i").is(itemId.prefix("a")) ++
|
fr"INNER JOIN" ++ RItem.table ++ fr"i ON" ++ RItem.Columns.id
|
||||||
|
.prefix("i")
|
||||||
|
.is(itemId.prefix("a")) ++
|
||||||
fr"WHERE" ++ and(itemId.prefix("a").is(id), RItem.Columns.cid.prefix("i").is(coll))
|
fr"WHERE" ++ and(itemId.prefix("a").is(id), RItem.Columns.cid.prefix("i").is(coll))
|
||||||
q.query[RAttachment].to[Vector]
|
q.query[RAttachment].to[Vector]
|
||||||
}
|
}
|
||||||
@ -65,7 +74,7 @@ object RAttachment {
|
|||||||
def delete(attachId: Ident): ConnectionIO[Int] =
|
def delete(attachId: Ident): ConnectionIO[Int] =
|
||||||
for {
|
for {
|
||||||
n0 <- RAttachmentMeta.delete(attachId)
|
n0 <- RAttachmentMeta.delete(attachId)
|
||||||
n1 <- deleteFrom(table, id is attachId).update.run
|
n1 <- deleteFrom(table, id.is(attachId)).update.run
|
||||||
} yield n0 + n1
|
} yield n0 + n1
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -7,12 +7,12 @@ import docspell.common._
|
|||||||
import docspell.store.impl._
|
import docspell.store.impl._
|
||||||
import docspell.store.impl.Implicits._
|
import docspell.store.impl.Implicits._
|
||||||
|
|
||||||
case class RAttachmentMeta(id: Ident
|
case class RAttachmentMeta(
|
||||||
, content: Option[String]
|
id: Ident,
|
||||||
, nerlabels: List[NerLabel]
|
content: Option[String],
|
||||||
, proposals: MetaProposalList) {
|
nerlabels: List[NerLabel],
|
||||||
|
proposals: MetaProposalList
|
||||||
}
|
) {}
|
||||||
|
|
||||||
object RAttachmentMeta {
|
object RAttachmentMeta {
|
||||||
def empty(attachId: Ident) = RAttachmentMeta(attachId, None, Nil, MetaProposalList.empty)
|
def empty(attachId: Ident) = RAttachmentMeta(attachId, None, Nil, MetaProposalList.empty)
|
||||||
@ -32,7 +32,7 @@ object RAttachmentMeta {
|
|||||||
insertRow(table, all, fr"${v.id},${v.content},${v.nerlabels},${v.proposals}").update.run
|
insertRow(table, all, fr"${v.id},${v.content},${v.nerlabels},${v.proposals}").update.run
|
||||||
|
|
||||||
def exists(attachId: Ident): ConnectionIO[Boolean] =
|
def exists(attachId: Ident): ConnectionIO[Boolean] =
|
||||||
selectCount(id, table, id is attachId).query[Int].unique.map(_ > 0)
|
selectCount(id, table, id.is(attachId)).query[Int].unique.map(_ > 0)
|
||||||
|
|
||||||
def upsert(v: RAttachmentMeta): ConnectionIO[Int] =
|
def upsert(v: RAttachmentMeta): ConnectionIO[Int] =
|
||||||
for {
|
for {
|
||||||
@ -41,22 +41,34 @@ object RAttachmentMeta {
|
|||||||
} yield n1
|
} yield n1
|
||||||
|
|
||||||
def update(v: RAttachmentMeta): ConnectionIO[Int] =
|
def update(v: RAttachmentMeta): ConnectionIO[Int] =
|
||||||
updateRow(table, id is v.id, commas(
|
updateRow(
|
||||||
content setTo v.content,
|
table,
|
||||||
nerlabels setTo v.nerlabels,
|
id.is(v.id),
|
||||||
proposals setTo v.proposals
|
commas(
|
||||||
)).update.run
|
content.setTo(v.content),
|
||||||
|
nerlabels.setTo(v.nerlabels),
|
||||||
|
proposals.setTo(v.proposals)
|
||||||
|
)
|
||||||
|
).update.run
|
||||||
|
|
||||||
def updateLabels(mid: Ident, labels: List[NerLabel]): ConnectionIO[Int] =
|
def updateLabels(mid: Ident, labels: List[NerLabel]): ConnectionIO[Int] =
|
||||||
updateRow(table, id is mid, commas(
|
updateRow(
|
||||||
nerlabels setTo labels
|
table,
|
||||||
)).update.run
|
id.is(mid),
|
||||||
|
commas(
|
||||||
|
nerlabels.setTo(labels)
|
||||||
|
)
|
||||||
|
).update.run
|
||||||
|
|
||||||
def updateProposals(mid: Ident, plist: MetaProposalList): ConnectionIO[Int] =
|
def updateProposals(mid: Ident, plist: MetaProposalList): ConnectionIO[Int] =
|
||||||
updateRow(table, id is mid, commas(
|
updateRow(
|
||||||
proposals setTo plist
|
table,
|
||||||
)).update.run
|
id.is(mid),
|
||||||
|
commas(
|
||||||
|
proposals.setTo(plist)
|
||||||
|
)
|
||||||
|
).update.run
|
||||||
|
|
||||||
def delete(attachId: Ident): ConnectionIO[Int] =
|
def delete(attachId: Ident): ConnectionIO[Int] =
|
||||||
deleteFrom(table, id is attachId).update.run
|
deleteFrom(table, id.is(attachId)).update.run
|
||||||
}
|
}
|
||||||
|
@ -7,10 +7,7 @@ import doobie._
|
|||||||
import doobie.implicits._
|
import doobie.implicits._
|
||||||
import fs2.Stream
|
import fs2.Stream
|
||||||
|
|
||||||
case class RCollective( id: Ident
|
case class RCollective(id: Ident, state: CollectiveState, language: Language, created: Timestamp)
|
||||||
, state: CollectiveState
|
|
||||||
, language: Language
|
|
||||||
, created: Timestamp)
|
|
||||||
|
|
||||||
object RCollective {
|
object RCollective {
|
||||||
|
|
||||||
@ -29,30 +26,38 @@ object RCollective {
|
|||||||
import Columns._
|
import Columns._
|
||||||
|
|
||||||
def insert(value: RCollective): ConnectionIO[Int] = {
|
def insert(value: RCollective): ConnectionIO[Int] = {
|
||||||
val sql = insertRow(table, Columns.all, fr"${value.id},${value.state},${value.language},${value.created}")
|
val sql = insertRow(
|
||||||
|
table,
|
||||||
|
Columns.all,
|
||||||
|
fr"${value.id},${value.state},${value.language},${value.created}"
|
||||||
|
)
|
||||||
sql.update.run
|
sql.update.run
|
||||||
}
|
}
|
||||||
|
|
||||||
def update(value: RCollective): ConnectionIO[Int] = {
|
def update(value: RCollective): ConnectionIO[Int] = {
|
||||||
val sql = updateRow(table, id is value.id, commas(
|
val sql = updateRow(
|
||||||
state setTo value.state
|
table,
|
||||||
))
|
id.is(value.id),
|
||||||
|
commas(
|
||||||
|
state.setTo(value.state)
|
||||||
|
)
|
||||||
|
)
|
||||||
sql.update.run
|
sql.update.run
|
||||||
}
|
}
|
||||||
|
|
||||||
def findLanguage(cid: Ident): ConnectionIO[Option[Language]] =
|
def findLanguage(cid: Ident): ConnectionIO[Option[Language]] =
|
||||||
selectSimple(List(language), table, id is cid).query[Option[Language]].unique
|
selectSimple(List(language), table, id.is(cid)).query[Option[Language]].unique
|
||||||
|
|
||||||
def updateLanguage(cid: Ident, lang: Language): ConnectionIO[Int] =
|
def updateLanguage(cid: Ident, lang: Language): ConnectionIO[Int] =
|
||||||
updateRow(table, id is cid, language setTo lang).update.run
|
updateRow(table, id.is(cid), language.setTo(lang)).update.run
|
||||||
|
|
||||||
def findById(cid: Ident): ConnectionIO[Option[RCollective]] = {
|
def findById(cid: Ident): ConnectionIO[Option[RCollective]] = {
|
||||||
val sql = selectSimple(all, table, id is cid)
|
val sql = selectSimple(all, table, id.is(cid))
|
||||||
sql.query[RCollective].option
|
sql.query[RCollective].option
|
||||||
}
|
}
|
||||||
|
|
||||||
def existsById(cid: Ident): ConnectionIO[Boolean] = {
|
def existsById(cid: Ident): ConnectionIO[Boolean] = {
|
||||||
val sql = selectCount(id, table, id is cid)
|
val sql = selectCount(id, table, id.is(cid))
|
||||||
sql.query[Int].unique.map(_ > 0)
|
sql.query[Int].unique.map(_ > 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -6,14 +6,13 @@ import docspell.store.impl._
|
|||||||
import docspell.store.impl.Implicits._
|
import docspell.store.impl.Implicits._
|
||||||
|
|
||||||
case class RContact(
|
case class RContact(
|
||||||
contactId: Ident
|
contactId: Ident,
|
||||||
, value: String
|
value: String,
|
||||||
, kind: ContactKind
|
kind: ContactKind,
|
||||||
, personId: Option[Ident]
|
personId: Option[Ident],
|
||||||
, orgId: Option[Ident]
|
orgId: Option[Ident],
|
||||||
, created: Timestamp) {
|
created: Timestamp
|
||||||
|
) {}
|
||||||
}
|
|
||||||
|
|
||||||
object RContact {
|
object RContact {
|
||||||
|
|
||||||
@ -26,48 +25,55 @@ object RContact {
|
|||||||
val personId = Column("pid")
|
val personId = Column("pid")
|
||||||
val orgId = Column("oid")
|
val orgId = Column("oid")
|
||||||
val created = Column("created")
|
val created = Column("created")
|
||||||
val all = List(contactId, value,kind, personId, orgId, created)
|
val all = List(contactId, value, kind, personId, orgId, created)
|
||||||
}
|
}
|
||||||
|
|
||||||
import Columns._
|
import Columns._
|
||||||
|
|
||||||
def insert(v: RContact): ConnectionIO[Int] = {
|
def insert(v: RContact): ConnectionIO[Int] = {
|
||||||
val sql = insertRow(table, all,
|
val sql = insertRow(
|
||||||
fr"${v.contactId},${v.value},${v.kind},${v.personId},${v.orgId},${v.created}")
|
table,
|
||||||
|
all,
|
||||||
|
fr"${v.contactId},${v.value},${v.kind},${v.personId},${v.orgId},${v.created}"
|
||||||
|
)
|
||||||
sql.update.run
|
sql.update.run
|
||||||
}
|
}
|
||||||
|
|
||||||
def update(v: RContact): ConnectionIO[Int] = {
|
def update(v: RContact): ConnectionIO[Int] = {
|
||||||
val sql = updateRow(table, contactId is v.contactId, commas(
|
val sql = updateRow(
|
||||||
value setTo v.value,
|
table,
|
||||||
kind setTo v.kind,
|
contactId.is(v.contactId),
|
||||||
personId setTo v.personId,
|
commas(
|
||||||
orgId setTo v.orgId
|
value.setTo(v.value),
|
||||||
))
|
kind.setTo(v.kind),
|
||||||
|
personId.setTo(v.personId),
|
||||||
|
orgId.setTo(v.orgId)
|
||||||
|
)
|
||||||
|
)
|
||||||
sql.update.run
|
sql.update.run
|
||||||
}
|
}
|
||||||
|
|
||||||
def delete(v: RContact): ConnectionIO[Int] =
|
def delete(v: RContact): ConnectionIO[Int] =
|
||||||
deleteFrom(table, contactId is v.contactId).update.run
|
deleteFrom(table, contactId.is(v.contactId)).update.run
|
||||||
|
|
||||||
def deleteOrg(oid: Ident): ConnectionIO[Int] =
|
def deleteOrg(oid: Ident): ConnectionIO[Int] =
|
||||||
deleteFrom(table, orgId is oid).update.run
|
deleteFrom(table, orgId.is(oid)).update.run
|
||||||
|
|
||||||
def deletePerson(pid: Ident): ConnectionIO[Int] =
|
def deletePerson(pid: Ident): ConnectionIO[Int] =
|
||||||
deleteFrom(table, personId is pid).update.run
|
deleteFrom(table, personId.is(pid)).update.run
|
||||||
|
|
||||||
def findById(id: Ident): ConnectionIO[Option[RContact]] = {
|
def findById(id: Ident): ConnectionIO[Option[RContact]] = {
|
||||||
val sql = selectSimple(all, table, contactId is id)
|
val sql = selectSimple(all, table, contactId.is(id))
|
||||||
sql.query[RContact].option
|
sql.query[RContact].option
|
||||||
}
|
}
|
||||||
|
|
||||||
def findAllPerson(pid: Ident): ConnectionIO[Vector[RContact]] = {
|
def findAllPerson(pid: Ident): ConnectionIO[Vector[RContact]] = {
|
||||||
val sql = selectSimple(all, table, personId is pid)
|
val sql = selectSimple(all, table, personId.is(pid))
|
||||||
sql.query[RContact].to[Vector]
|
sql.query[RContact].to[Vector]
|
||||||
}
|
}
|
||||||
|
|
||||||
def findAllOrg(oid: Ident): ConnectionIO[Vector[RContact]] = {
|
def findAllOrg(oid: Ident): ConnectionIO[Vector[RContact]] = {
|
||||||
val sql = selectSimple(all, table, orgId is oid)
|
val sql = selectSimple(all, table, orgId.is(oid))
|
||||||
sql.query[RContact].to[Vector]
|
sql.query[RContact].to[Vector]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,13 +5,7 @@ import docspell.common._
|
|||||||
import docspell.store.impl._
|
import docspell.store.impl._
|
||||||
import docspell.store.impl.Implicits._
|
import docspell.store.impl.Implicits._
|
||||||
|
|
||||||
case class REquipment(
|
case class REquipment(eid: Ident, cid: Ident, name: String, created: Timestamp) {}
|
||||||
eid: Ident
|
|
||||||
, cid: Ident
|
|
||||||
, name: String
|
|
||||||
, created: Timestamp) {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
object REquipment {
|
object REquipment {
|
||||||
|
|
||||||
@ -22,44 +16,47 @@ object REquipment {
|
|||||||
val cid = Column("cid")
|
val cid = Column("cid")
|
||||||
val name = Column("name")
|
val name = Column("name")
|
||||||
val created = Column("created")
|
val created = Column("created")
|
||||||
val all = List(eid,cid,name,created)
|
val all = List(eid, cid, name, created)
|
||||||
}
|
}
|
||||||
import Columns._
|
import Columns._
|
||||||
|
|
||||||
def insert(v: REquipment): ConnectionIO[Int] = {
|
def insert(v: REquipment): ConnectionIO[Int] = {
|
||||||
val sql = insertRow(table, all,
|
val sql = insertRow(table, all, fr"${v.eid},${v.cid},${v.name},${v.created}")
|
||||||
fr"${v.eid},${v.cid},${v.name},${v.created}")
|
|
||||||
sql.update.run
|
sql.update.run
|
||||||
}
|
}
|
||||||
|
|
||||||
def update(v: REquipment): ConnectionIO[Int] = {
|
def update(v: REquipment): ConnectionIO[Int] = {
|
||||||
val sql = updateRow(table, and(eid is v.eid, cid is v.cid), commas(
|
val sql = updateRow(
|
||||||
cid setTo v.cid,
|
table,
|
||||||
name setTo v.name
|
and(eid.is(v.eid), cid.is(v.cid)),
|
||||||
))
|
commas(
|
||||||
|
cid.setTo(v.cid),
|
||||||
|
name.setTo(v.name)
|
||||||
|
)
|
||||||
|
)
|
||||||
sql.update.run
|
sql.update.run
|
||||||
}
|
}
|
||||||
|
|
||||||
def existsByName(coll: Ident, ename: String): ConnectionIO[Boolean] = {
|
def existsByName(coll: Ident, ename: String): ConnectionIO[Boolean] = {
|
||||||
val sql = selectCount(eid, table, and(cid is coll, name is ename))
|
val sql = selectCount(eid, table, and(cid.is(coll), name.is(ename)))
|
||||||
sql.query[Int].unique.map(_ > 0)
|
sql.query[Int].unique.map(_ > 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
def findById(id: Ident): ConnectionIO[Option[REquipment]] = {
|
def findById(id: Ident): ConnectionIO[Option[REquipment]] = {
|
||||||
val sql = selectSimple(all, table, eid is id)
|
val sql = selectSimple(all, table, eid.is(id))
|
||||||
sql.query[REquipment].option
|
sql.query[REquipment].option
|
||||||
}
|
}
|
||||||
|
|
||||||
def findAll(coll: Ident, order: Columns.type => Column): ConnectionIO[Vector[REquipment]] = {
|
def findAll(coll: Ident, order: Columns.type => Column): ConnectionIO[Vector[REquipment]] = {
|
||||||
val sql = selectSimple(all, table, cid is coll) ++ orderBy(order(Columns).f)
|
val sql = selectSimple(all, table, cid.is(coll)) ++ orderBy(order(Columns).f)
|
||||||
sql.query[REquipment].to[Vector]
|
sql.query[REquipment].to[Vector]
|
||||||
}
|
}
|
||||||
|
|
||||||
def findLike(coll: Ident, equipName: String): ConnectionIO[Vector[IdRef]] =
|
def findLike(coll: Ident, equipName: String): ConnectionIO[Vector[IdRef]] =
|
||||||
selectSimple(List(eid, name), table, and(cid is coll,
|
selectSimple(List(eid, name), table, and(cid.is(coll), name.lowerLike(equipName)))
|
||||||
name.lowerLike(equipName))).
|
.query[IdRef]
|
||||||
query[IdRef].to[Vector]
|
.to[Vector]
|
||||||
|
|
||||||
def delete(id: Ident, coll: Ident): ConnectionIO[Int] =
|
def delete(id: Ident, coll: Ident): ConnectionIO[Int] =
|
||||||
deleteFrom(table, and(eid is id, cid is coll)).update.run
|
deleteFrom(table, and(eid.is(id), cid.is(coll))).update.run
|
||||||
}
|
}
|
||||||
|
@ -8,9 +8,7 @@ import docspell.common._
|
|||||||
import docspell.store.impl._
|
import docspell.store.impl._
|
||||||
import docspell.store.impl.Implicits._
|
import docspell.store.impl.Implicits._
|
||||||
|
|
||||||
case class RInvitation(id: Ident, created: Timestamp) {
|
case class RInvitation(id: Ident, created: Timestamp) {}
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
object RInvitation {
|
object RInvitation {
|
||||||
|
|
||||||
@ -33,18 +31,16 @@ object RInvitation {
|
|||||||
insertRow(table, all, fr"${v.id},${v.created}").update.run
|
insertRow(table, all, fr"${v.id},${v.created}").update.run
|
||||||
|
|
||||||
def insertNew: ConnectionIO[RInvitation] =
|
def insertNew: ConnectionIO[RInvitation] =
|
||||||
generate[ConnectionIO].
|
generate[ConnectionIO].flatMap(v => insert(v).map(_ => v))
|
||||||
flatMap(v => insert(v).map(_ => v))
|
|
||||||
|
|
||||||
def findById(invite: Ident): ConnectionIO[Option[RInvitation]] =
|
def findById(invite: Ident): ConnectionIO[Option[RInvitation]] =
|
||||||
selectSimple(all, table, id is invite).query[RInvitation].option
|
selectSimple(all, table, id.is(invite)).query[RInvitation].option
|
||||||
|
|
||||||
def delete(invite: Ident): ConnectionIO[Int] =
|
def delete(invite: Ident): ConnectionIO[Int] =
|
||||||
deleteFrom(table, id is invite).update.run
|
deleteFrom(table, id.is(invite)).update.run
|
||||||
|
|
||||||
def useInvite(invite: Ident, minCreated: Timestamp): ConnectionIO[Boolean] = {
|
def useInvite(invite: Ident, minCreated: Timestamp): ConnectionIO[Boolean] = {
|
||||||
val get = selectCount(id, table, and(id is invite, created isGt minCreated)).
|
val get = selectCount(id, table, and(id.is(invite), created.isGt(minCreated))).query[Int].unique
|
||||||
query[Int].unique
|
|
||||||
for {
|
for {
|
||||||
inv <- get
|
inv <- get
|
||||||
_ <- delete(invite)
|
_ <- delete(invite)
|
||||||
|
@ -8,32 +8,55 @@ import docspell.common._
|
|||||||
import docspell.store.impl._
|
import docspell.store.impl._
|
||||||
import docspell.store.impl.Implicits._
|
import docspell.store.impl.Implicits._
|
||||||
|
|
||||||
case class RItem( id: Ident
|
case class RItem(
|
||||||
, cid: Ident
|
id: Ident,
|
||||||
, name: String
|
cid: Ident,
|
||||||
, itemDate: Option[Timestamp]
|
name: String,
|
||||||
, source: String
|
itemDate: Option[Timestamp],
|
||||||
, direction: Direction
|
source: String,
|
||||||
, state: ItemState
|
direction: Direction,
|
||||||
, corrOrg: Option[Ident]
|
state: ItemState,
|
||||||
, corrPerson: Option[Ident]
|
corrOrg: Option[Ident],
|
||||||
, concPerson: Option[Ident]
|
corrPerson: Option[Ident],
|
||||||
, concEquipment: Option[Ident]
|
concPerson: Option[Ident],
|
||||||
, inReplyTo: Option[Ident]
|
concEquipment: Option[Ident],
|
||||||
, dueDate: Option[Timestamp]
|
inReplyTo: Option[Ident],
|
||||||
, created: Timestamp
|
dueDate: Option[Timestamp],
|
||||||
, updated: Timestamp
|
created: Timestamp,
|
||||||
, notes: Option[String]) {
|
updated: Timestamp,
|
||||||
|
notes: Option[String]
|
||||||
}
|
) {}
|
||||||
|
|
||||||
object RItem {
|
object RItem {
|
||||||
|
|
||||||
def newItem[F[_]: Sync](cid: Ident, name: String, source: String, direction: Direction, state: ItemState): F[RItem] =
|
def newItem[F[_]: Sync](
|
||||||
|
cid: Ident,
|
||||||
|
name: String,
|
||||||
|
source: String,
|
||||||
|
direction: Direction,
|
||||||
|
state: ItemState
|
||||||
|
): F[RItem] =
|
||||||
for {
|
for {
|
||||||
now <- Timestamp.current[F]
|
now <- Timestamp.current[F]
|
||||||
id <- Ident.randomId[F]
|
id <- Ident.randomId[F]
|
||||||
} yield RItem(id, cid, name, None, source, direction, state, None, None, None, None, None, None, now, now, None)
|
} yield RItem(
|
||||||
|
id,
|
||||||
|
cid,
|
||||||
|
name,
|
||||||
|
None,
|
||||||
|
source,
|
||||||
|
direction,
|
||||||
|
state,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
now,
|
||||||
|
now,
|
||||||
|
None
|
||||||
|
)
|
||||||
|
|
||||||
val table = fr"item"
|
val table = fr"item"
|
||||||
|
|
||||||
@ -54,110 +77,189 @@ object RItem {
|
|||||||
val created = Column("created")
|
val created = Column("created")
|
||||||
val updated = Column("updated")
|
val updated = Column("updated")
|
||||||
val notes = Column("notes")
|
val notes = Column("notes")
|
||||||
val all = List(id, cid, name, itemDate, source, incoming, state, corrOrg,
|
val all = List(
|
||||||
corrPerson, concPerson, concEquipment, inReplyTo, dueDate, created, updated, notes)
|
id,
|
||||||
|
cid,
|
||||||
|
name,
|
||||||
|
itemDate,
|
||||||
|
source,
|
||||||
|
incoming,
|
||||||
|
state,
|
||||||
|
corrOrg,
|
||||||
|
corrPerson,
|
||||||
|
concPerson,
|
||||||
|
concEquipment,
|
||||||
|
inReplyTo,
|
||||||
|
dueDate,
|
||||||
|
created,
|
||||||
|
updated,
|
||||||
|
notes
|
||||||
|
)
|
||||||
}
|
}
|
||||||
import Columns._
|
import Columns._
|
||||||
|
|
||||||
def insert(v: RItem): ConnectionIO[Int] =
|
def insert(v: RItem): ConnectionIO[Int] =
|
||||||
insertRow(table, all, fr"${v.id},${v.cid},${v.name},${v.itemDate},${v.source},${v.direction},${v.state}," ++
|
insertRow(
|
||||||
|
table,
|
||||||
|
all,
|
||||||
|
fr"${v.id},${v.cid},${v.name},${v.itemDate},${v.source},${v.direction},${v.state}," ++
|
||||||
fr"${v.corrOrg},${v.corrPerson},${v.concPerson},${v.concEquipment},${v.inReplyTo},${v.dueDate}," ++
|
fr"${v.corrOrg},${v.corrPerson},${v.concPerson},${v.concEquipment},${v.inReplyTo},${v.dueDate}," ++
|
||||||
fr"${v.created},${v.updated},${v.notes}").update.run
|
fr"${v.created},${v.updated},${v.notes}"
|
||||||
|
).update.run
|
||||||
|
|
||||||
def getCollective(itemId: Ident): ConnectionIO[Option[Ident]] =
|
def getCollective(itemId: Ident): ConnectionIO[Option[Ident]] =
|
||||||
selectSimple(List(cid), table, id is itemId).query[Ident].option
|
selectSimple(List(cid), table, id.is(itemId)).query[Ident].option
|
||||||
|
|
||||||
def updateState(itemId: Ident, itemState: ItemState): ConnectionIO[Int] =
|
def updateState(itemId: Ident, itemState: ItemState): ConnectionIO[Int] =
|
||||||
for {
|
for {
|
||||||
t <- currentTime
|
t <- currentTime
|
||||||
n <- updateRow(table, id is itemId, commas(state setTo itemState, updated setTo t)).update.run
|
n <- updateRow(table, id.is(itemId), commas(state.setTo(itemState), updated.setTo(t))).update.run
|
||||||
} yield n
|
} yield n
|
||||||
|
|
||||||
def updateStateForCollective(itemId: Ident, itemState: ItemState, coll: Ident): ConnectionIO[Int] =
|
def updateStateForCollective(
|
||||||
|
itemId: Ident,
|
||||||
|
itemState: ItemState,
|
||||||
|
coll: Ident
|
||||||
|
): ConnectionIO[Int] =
|
||||||
for {
|
for {
|
||||||
t <- currentTime
|
t <- currentTime
|
||||||
n <- updateRow(table, and(id is itemId, cid is coll), commas(state setTo itemState, updated setTo t)).update.run
|
n <- updateRow(
|
||||||
|
table,
|
||||||
|
and(id.is(itemId), cid.is(coll)),
|
||||||
|
commas(state.setTo(itemState), updated.setTo(t))
|
||||||
|
).update.run
|
||||||
} yield n
|
} yield n
|
||||||
|
|
||||||
def updateDirection(itemId: Ident, coll: Ident, dir: Direction): ConnectionIO[Int] =
|
def updateDirection(itemId: Ident, coll: Ident, dir: Direction): ConnectionIO[Int] =
|
||||||
for {
|
for {
|
||||||
t <- currentTime
|
t <- currentTime
|
||||||
n <- updateRow(table, and(id is itemId, cid is coll), commas(incoming setTo dir, updated setTo t)).update.run
|
n <- updateRow(
|
||||||
|
table,
|
||||||
|
and(id.is(itemId), cid.is(coll)),
|
||||||
|
commas(incoming.setTo(dir), updated.setTo(t))
|
||||||
|
).update.run
|
||||||
} yield n
|
} yield n
|
||||||
|
|
||||||
|
|
||||||
def updateCorrOrg(itemId: Ident, coll: Ident, org: Option[Ident]): ConnectionIO[Int] =
|
def updateCorrOrg(itemId: Ident, coll: Ident, org: Option[Ident]): ConnectionIO[Int] =
|
||||||
for {
|
for {
|
||||||
t <- currentTime
|
t <- currentTime
|
||||||
n <- updateRow(table, and(id is itemId, cid is coll), commas(corrOrg setTo org, updated setTo t)).update.run
|
n <- updateRow(
|
||||||
|
table,
|
||||||
|
and(id.is(itemId), cid.is(coll)),
|
||||||
|
commas(corrOrg.setTo(org), updated.setTo(t))
|
||||||
|
).update.run
|
||||||
} yield n
|
} yield n
|
||||||
|
|
||||||
def removeCorrOrg(coll: Ident, currentOrg: Ident): ConnectionIO[Int] =
|
def removeCorrOrg(coll: Ident, currentOrg: Ident): ConnectionIO[Int] =
|
||||||
for {
|
for {
|
||||||
t <- currentTime
|
t <- currentTime
|
||||||
n <- updateRow(table, and(cid is coll, corrOrg is Some(currentOrg)), commas(corrOrg setTo(None: Option[Ident]), updated setTo t)).update.run
|
n <- updateRow(
|
||||||
|
table,
|
||||||
|
and(cid.is(coll), corrOrg.is(Some(currentOrg))),
|
||||||
|
commas(corrOrg.setTo(None: Option[Ident]), updated.setTo(t))
|
||||||
|
).update.run
|
||||||
} yield n
|
} yield n
|
||||||
|
|
||||||
def updateCorrPerson(itemId: Ident, coll: Ident, person: Option[Ident]): ConnectionIO[Int] =
|
def updateCorrPerson(itemId: Ident, coll: Ident, person: Option[Ident]): ConnectionIO[Int] =
|
||||||
for {
|
for {
|
||||||
t <- currentTime
|
t <- currentTime
|
||||||
n <- updateRow(table, and(id is itemId, cid is coll), commas(corrPerson setTo person, updated setTo t)).update.run
|
n <- updateRow(
|
||||||
|
table,
|
||||||
|
and(id.is(itemId), cid.is(coll)),
|
||||||
|
commas(corrPerson.setTo(person), updated.setTo(t))
|
||||||
|
).update.run
|
||||||
} yield n
|
} yield n
|
||||||
|
|
||||||
def removeCorrPerson(coll: Ident, currentPerson: Ident): ConnectionIO[Int] =
|
def removeCorrPerson(coll: Ident, currentPerson: Ident): ConnectionIO[Int] =
|
||||||
for {
|
for {
|
||||||
t <- currentTime
|
t <- currentTime
|
||||||
n <- updateRow(table, and(cid is coll, corrPerson is Some(currentPerson)), commas(corrPerson setTo(None: Option[Ident]), updated setTo t)).update.run
|
n <- updateRow(
|
||||||
|
table,
|
||||||
|
and(cid.is(coll), corrPerson.is(Some(currentPerson))),
|
||||||
|
commas(corrPerson.setTo(None: Option[Ident]), updated.setTo(t))
|
||||||
|
).update.run
|
||||||
} yield n
|
} yield n
|
||||||
|
|
||||||
def updateConcPerson(itemId: Ident, coll: Ident, person: Option[Ident]): ConnectionIO[Int] =
|
def updateConcPerson(itemId: Ident, coll: Ident, person: Option[Ident]): ConnectionIO[Int] =
|
||||||
for {
|
for {
|
||||||
t <- currentTime
|
t <- currentTime
|
||||||
n <- updateRow(table, and(id is itemId, cid is coll), commas(concPerson setTo person, updated setTo t)).update.run
|
n <- updateRow(
|
||||||
|
table,
|
||||||
|
and(id.is(itemId), cid.is(coll)),
|
||||||
|
commas(concPerson.setTo(person), updated.setTo(t))
|
||||||
|
).update.run
|
||||||
} yield n
|
} yield n
|
||||||
|
|
||||||
def removeConcPerson(coll: Ident, currentPerson: Ident): ConnectionIO[Int] =
|
def removeConcPerson(coll: Ident, currentPerson: Ident): ConnectionIO[Int] =
|
||||||
for {
|
for {
|
||||||
t <- currentTime
|
t <- currentTime
|
||||||
n <- updateRow(table, and(cid is coll, concPerson is Some(currentPerson)), commas(concPerson setTo(None: Option[Ident]), updated setTo t)).update.run
|
n <- updateRow(
|
||||||
|
table,
|
||||||
|
and(cid.is(coll), concPerson.is(Some(currentPerson))),
|
||||||
|
commas(concPerson.setTo(None: Option[Ident]), updated.setTo(t))
|
||||||
|
).update.run
|
||||||
} yield n
|
} yield n
|
||||||
|
|
||||||
def updateConcEquip(itemId: Ident, coll: Ident, equip: Option[Ident]): ConnectionIO[Int] =
|
def updateConcEquip(itemId: Ident, coll: Ident, equip: Option[Ident]): ConnectionIO[Int] =
|
||||||
for {
|
for {
|
||||||
t <- currentTime
|
t <- currentTime
|
||||||
n <- updateRow(table, and(id is itemId, cid is coll), commas(concEquipment setTo equip, updated setTo t)).update.run
|
n <- updateRow(
|
||||||
|
table,
|
||||||
|
and(id.is(itemId), cid.is(coll)),
|
||||||
|
commas(concEquipment.setTo(equip), updated.setTo(t))
|
||||||
|
).update.run
|
||||||
} yield n
|
} yield n
|
||||||
|
|
||||||
def removeConcEquip(coll: Ident, currentEquip: Ident): ConnectionIO[Int] =
|
def removeConcEquip(coll: Ident, currentEquip: Ident): ConnectionIO[Int] =
|
||||||
for {
|
for {
|
||||||
t <- currentTime
|
t <- currentTime
|
||||||
n <- updateRow(table, and(cid is coll, concEquipment is Some(currentEquip)), commas(concPerson setTo(None: Option[Ident]), updated setTo t)).update.run
|
n <- updateRow(
|
||||||
|
table,
|
||||||
|
and(cid.is(coll), concEquipment.is(Some(currentEquip))),
|
||||||
|
commas(concPerson.setTo(None: Option[Ident]), updated.setTo(t))
|
||||||
|
).update.run
|
||||||
} yield n
|
} yield n
|
||||||
|
|
||||||
def updateNotes(itemId: Ident, coll: Ident, text: Option[String]): ConnectionIO[Int] =
|
def updateNotes(itemId: Ident, coll: Ident, text: Option[String]): ConnectionIO[Int] =
|
||||||
for {
|
for {
|
||||||
t <- currentTime
|
t <- currentTime
|
||||||
n <- updateRow(table, and(id is itemId, cid is coll), commas(notes setTo text, updated setTo t)).update.run
|
n <- updateRow(
|
||||||
|
table,
|
||||||
|
and(id.is(itemId), cid.is(coll)),
|
||||||
|
commas(notes.setTo(text), updated.setTo(t))
|
||||||
|
).update.run
|
||||||
} yield n
|
} yield n
|
||||||
|
|
||||||
def updateName(itemId: Ident, coll: Ident, itemName: String): ConnectionIO[Int] =
|
def updateName(itemId: Ident, coll: Ident, itemName: String): ConnectionIO[Int] =
|
||||||
for {
|
for {
|
||||||
t <- currentTime
|
t <- currentTime
|
||||||
n <- updateRow(table, and(id is itemId, cid is coll), commas(name setTo itemName, updated setTo t)).update.run
|
n <- updateRow(
|
||||||
|
table,
|
||||||
|
and(id.is(itemId), cid.is(coll)),
|
||||||
|
commas(name.setTo(itemName), updated.setTo(t))
|
||||||
|
).update.run
|
||||||
} yield n
|
} yield n
|
||||||
|
|
||||||
def updateDate(itemId: Ident, coll: Ident, date: Option[Timestamp]): ConnectionIO[Int] =
|
def updateDate(itemId: Ident, coll: Ident, date: Option[Timestamp]): ConnectionIO[Int] =
|
||||||
for {
|
for {
|
||||||
t <- currentTime
|
t <- currentTime
|
||||||
n <- updateRow(table, and(id is itemId, cid is coll), commas(itemDate setTo date, updated setTo t)).update.run
|
n <- updateRow(
|
||||||
|
table,
|
||||||
|
and(id.is(itemId), cid.is(coll)),
|
||||||
|
commas(itemDate.setTo(date), updated.setTo(t))
|
||||||
|
).update.run
|
||||||
} yield n
|
} yield n
|
||||||
|
|
||||||
def updateDueDate(itemId: Ident, coll: Ident, date: Option[Timestamp]): ConnectionIO[Int] =
|
def updateDueDate(itemId: Ident, coll: Ident, date: Option[Timestamp]): ConnectionIO[Int] =
|
||||||
for {
|
for {
|
||||||
t <- currentTime
|
t <- currentTime
|
||||||
n <- updateRow(table, and(id is itemId, cid is coll), commas(dueDate setTo date, updated setTo t)).update.run
|
n <- updateRow(
|
||||||
|
table,
|
||||||
|
and(id.is(itemId), cid.is(coll)),
|
||||||
|
commas(dueDate.setTo(date), updated.setTo(t))
|
||||||
|
).update.run
|
||||||
} yield n
|
} yield n
|
||||||
|
|
||||||
def deleteByIdAndCollective(itemId: Ident, coll: Ident): ConnectionIO[Int] =
|
def deleteByIdAndCollective(itemId: Ident, coll: Ident): ConnectionIO[Int] =
|
||||||
deleteFrom(table, and(id is itemId, cid is coll)).update.run
|
deleteFrom(table, and(id.is(itemId), cid.is(coll))).update.run
|
||||||
}
|
}
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user