Skip to content

Commit

Permalink
Merge pull request #462 from eikek/alias-member
Browse files Browse the repository at this point in the history
Alias member
  • Loading branch information
mergify[bot] authored May 21, 2021
2 parents 2762420 + f149b09 commit d47ba33
Show file tree
Hide file tree
Showing 38 changed files with 1,482 additions and 236 deletions.
1 change: 1 addition & 0 deletions elm.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"dependencies": {
"direct": {
"NoRedInk/elm-json-decode-pipeline": "1.0.0",
"NoRedInk/elm-simple-fuzzy": "1.0.3",
"elm/browser": "1.0.2",
"elm/core": "1.0.5",
"elm/file": "1.0.5",
Expand Down
79 changes: 61 additions & 18 deletions modules/backend/src/main/scala/sharry/backend/alias/OAlias.scala
Original file line number Diff line number Diff line change
@@ -1,60 +1,103 @@
package sharry.backend.alias

import cats.data.OptionT
import cats.effect._
import cats.implicits._
import fs2.Stream

import sharry.backend.alias.OAlias.{AliasDetail, AliasInput}
import sharry.common._
import sharry.common.syntax.all._
import sharry.store.AddResult
import sharry.store.Store
import sharry.store.records.RAlias
import sharry.store.records.RAliasMember

import doobie._
import org.log4s._

trait OAlias[F[_]] {

def create(alias: RAlias): F[AddResult]
def create(alias: AliasInput): F[AddResult]

def createF(alias: F[RAlias]): F[AddResult]
def createF(alias: F[AliasInput]): F[AddResult]

def modify(aliasId: Ident, accId: Ident, alias: RAlias): F[AddResult]
def modify(aliasId: Ident, accId: Ident, alias: AliasInput): F[AddResult]

def findAll(accId: Ident, nameQuery: String): Stream[F, RAlias]
def findAll(accId: Ident, nameQuery: String): Stream[F, AliasDetail]

def findById(id: Ident, accId: Ident): F[Option[RAlias]]
def findById(id: Ident, accId: Ident): F[Option[AliasDetail]]

def delete(id: Ident, accId: Ident): F[Boolean]
}

object OAlias {
private[this] val logger = getLogger

/** Details about an alias including a list of user-ids that make up its members. */
case class AliasInput(alias: RAlias, members: List[Ident])

case class AliasDetail(alias: RAlias, ownerLogin: Ident, members: List[AliasMember])

case class AliasMember(accountId: Ident, login: Ident)

def apply[F[_]: Effect](store: Store[F]): Resource[F, OAlias[F]] =
Resource.pure[F, OAlias[F]](new OAlias[F] {
def create(alias: RAlias): F[AddResult] =
store.add(RAlias.insert(alias), RAlias.existsById(alias.id))

def createF(alias: F[RAlias]): F[AddResult] =
def create(detail: AliasInput): F[AddResult] =
store.add(
for {
n <- RAlias.insert(detail.alias)
k <-
if (n > 0)
RAliasMember.insertForAlias(detail.alias.id, detail.members)
else 0.pure[ConnectionIO]
} yield n + k,
RAlias.existsById(detail.alias.id)
)

def createF(alias: F[AliasInput]): F[AddResult] =
alias.flatMap(create)

def modify(aliasId: Ident, accId: Ident, alias: RAlias): F[AddResult] = {
val exists = RAlias.existsById(alias.id)
val modify = RAlias.update(aliasId, accId, alias)
def modify(
aliasId: Ident,
accId: Ident,
detail: AliasInput
): F[AddResult] = {
val doUpdate = for {
n <- RAlias.update(aliasId, accId, detail.alias)
k <-
if (n > 0) RAliasMember.updateForAlias(aliasId, detail.members)
else 0.pure[ConnectionIO]
} yield n + k

for {
_ <- logger.fdebug(s"Modify alias '${aliasId.id}' to $alias")
res <- store.add(modify, exists)
_ <- logger.fdebug(s"Modify alias '${aliasId.id}' to ${detail.alias}")
n <- store.transact(doUpdate)
res =
if (n > 0) AddResult.Success
else AddResult.Failure(new Exception("No rows modified!"))
} yield res
}

def findAll(accId: Ident, nameQuery: String): Stream[F, RAlias] =
store.transact(RAlias.findAll(accId, nameQuery))
def findAll(
accId: Ident,
nameQuery: String
): Stream[F, AliasDetail] =
store
.transact(RAlias.findAll(accId, nameQuery).evalMap(loadMembers))

def findById(id: Ident, accId: Ident): F[Option[RAlias]] =
store.transact(RAlias.findById(id, accId))
def findById(id: Ident, accId: Ident): F[Option[AliasDetail]] =
store.transact(OptionT(RAlias.findById(id, accId)).semiflatMap(loadMembers).value)

def delete(id: Ident, accId: Ident): F[Boolean] =
store.transact(RAlias.delete(id, accId)).map(_ > 0)

private def loadMembers(alias: (RAlias, Ident)): ConnectionIO[AliasDetail] =
for {
members <- RAliasMember.findForAlias(alias._1.id)
conv = members.map(t => AliasMember(t._1.accountId, t._2))
} yield AliasDetail(alias._1, alias._2, conv)

})

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package sharry.backend.mail

import sharry.backend.mail.NotifyData.AccountInfo
import sharry.common._

final case class NotifyData(
aliasId: Ident,
aliasName: String,
users: List[AccountInfo]
)

object NotifyData {

final case class AccountInfo(login: Ident, email: String)
}
Original file line number Diff line number Diff line change
@@ -1,19 +1,36 @@
package sharry.backend.mail

sealed trait NotifyResult {}
import emil.MailAddress

object NotifyResult {
def missingEmail: NotifyResult = MissingEmail
def featureDisabled: NotifyResult = FeatureDisabled

case object InvalidAlias extends NotifyResult

case object FeatureDisabled extends NotifyResult
sealed trait NotifyResult {
def isSuccess: Boolean
def isError: Boolean =
!isSuccess

case object MissingEmail extends NotifyResult
def receiver: Option[MailAddress]
}

case class SendFailed(err: String) extends NotifyResult
object NotifyResult {
def featureDisabled: NotifyResult = FeatureDisabled

case object SendSuccessful extends NotifyResult
case object InvalidAlias extends NotifyResult {
val isSuccess = false
def receiver: Option[MailAddress] = None
}

case object FeatureDisabled extends NotifyResult {
val isSuccess = false
def receiver: Option[MailAddress] = None
}

case class SendFailed(mail: MailAddress, err: String) extends NotifyResult {
val isSuccess = false
def receiver: Option[MailAddress] = Some(mail)
}

case class SendSuccessful(mail: MailAddress) extends NotifyResult {
val isSuccess = true
def receiver: Option[MailAddress] = Some(mail)
}

}
44 changes: 31 additions & 13 deletions modules/backend/src/main/scala/sharry/backend/mail/OMail.scala
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import cats.implicits._

import sharry.backend.mail.MailConfig.MailTpl
import sharry.common._
import sharry.common.syntax.all._
import sharry.store.Store

import emil.builder._
Expand All @@ -21,7 +22,7 @@ trait OMail[F[_]] {
aliasId: Ident,
shareId: Ident,
baseUrl: LenientUri
): F[NotifyResult]
): F[List[NotifyResult]]

def getShareTemplate(
acc: AccountId,
Expand Down Expand Up @@ -52,7 +53,7 @@ object OMail {
aliasId: Ident,
shareId: Ident,
baseUrl: LenientUri
): F[NotifyResult] = {
): F[List[NotifyResult]] = {
def createMail(tpl: MailTpl, data: TemplateData, receiver: MailAddress): Mail[F] =
MailBuilder.build(
From(cfg.smtp.defaultFrom.getOrElse(receiver)),
Expand All @@ -67,24 +68,41 @@ object OMail {
.send(createMail(cfg.templates.uploadNotify, td, rec))
.attempt
.map {
case Right(_) => NotifyResult.SendSuccessful
case Right(_) => NotifyResult.SendSuccessful(rec)
case Left(ex) =>
logger.warn(ex)("Sending failed")
NotifyResult.SendFailed(ex.getMessage)
NotifyResult.SendFailed(rec, ex.getMessage)
}

if (!cfg.enabled) NotifyResult.featureDisabled.pure[F]
if (!cfg.enabled) List(NotifyResult.featureDisabled).pure[F]
else
(for {
t <- OptionT(store.transact(Queries.resolveAlias(aliasId, shareId)))
receiver = t._2.email.map(MailAddress.parse).flatMap(_.toOption)
td = TemplateData(t._2.login.value, baseUrl / shareId.id, false, t._1.name)
res <- OptionT.liftF(
receiver
.map(rec => send(rec, td))
.getOrElse(NotifyResult.missingEmail.pure[F])
data <- OptionT(store.transact(Queries.findNotifyData(aliasId, shareId)))
receivers <- OptionT.fromOption[F](
data.users.traverse(u =>
MailAddress.parse(u.email).toOption.map(ma => (u.login, ma))
)
)
} yield res).getOrElse(NotifyResult.InvalidAlias)
templates = receivers.map { case (login, mailAddress) =>
(
mailAddress,
TemplateData(login, baseUrl / shareId.id, false, data.aliasName)
)
}
res <- OptionT.liftF(templates.traverse((send _).tupled))
failedReceiver = res
.filter(_.isError)
.flatMap(_.receiver)
.map(_.displayString)
.mkString(", ")
_ <- OptionT.liftF(
logger.finfo(
"Send notification mails about upload. " +
s"Success ${res.filter(_.isSuccess).size}/${res.size}. " +
s"Sending failures for: ${failedReceiver}"
)
)
} yield res).getOrElse(List(NotifyResult.InvalidAlias))
}

def getShareTemplate(
Expand Down
57 changes: 44 additions & 13 deletions modules/backend/src/main/scala/sharry/backend/mail/Queries.scala
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package sharry.backend.mail

import cats.data.OptionT

import sharry.common._
import sharry.store.doobie.DoobieMeta._
import sharry.store.doobie._
Expand All @@ -11,28 +13,57 @@ import emil.MailAddress

object Queries {

def resolveAlias(
aliasId: Ident,
shareId: Ident
): ConnectionIO[Option[(RAlias, RAccount)]] = {
def findNotifyData(aliasId: Ident, shareId: Ident): ConnectionIO[Option[NotifyData]] = {
val aId = "a" :: RAlias.Columns.id
val aAccount = "a" :: RAlias.Columns.account
val aName = "a" :: RAlias.Columns.name
val uId = "u" :: RAccount.Columns.id
val uLogin = "u" :: RAccount.Columns.login
val uEmail = "u" :: RAccount.Columns.email
val sId = "s" :: RShare.Columns.id
val sAlias = "s" :: RShare.Columns.aliasId
val mAlias = "m" :: RAliasMember.Columns.aliasId
val mAccount = "m" :: RAliasMember.Columns.accountId

val from = RAlias.table ++ fr"a INNER JOIN" ++
RAccount.table ++ fr"u ON" ++ uId.is(aAccount) ++ fr"INNER JOIN" ++
RShare.table ++ fr"s ON" ++ sAlias.is(aliasId)
val baseQuery =
Sql
.selectSimple(
Seq(aId, aName),
RAlias.table ++ fr"a" ++
fr"INNER JOIN" ++ RShare.table ++ fr"s ON" ++ sAlias.is(aId),
Sql.and(aId.is(aliasId), sId.is(shareId))
)
.query[(Ident, String)]
.option

Sql
.selectSimple(
RAlias.Columns.all.map("a" :: _) ++ RAccount.Columns.all.map("u" :: _),
val memberQuery: Fragment = {
val from = RAliasMember.table ++ fr"m" ++
fr"INNER JOIN" ++ RAccount.table ++ fr"u ON" ++ uId.is(mAccount)
Sql
.selectSimple(
Seq(uLogin, uEmail),
from,
Sql.and(mAlias.is(aliasId), uEmail.isNotNull)
)
}
val ownerQuery: Fragment = {
val from = RAlias.table ++ fr"a" ++
fr"INNER JOIN" ++ RAccount.table ++ fr"u ON" ++ uId.is(aAccount)
Sql.selectSimple(
Seq(uLogin, uEmail),
from,
Sql.and(aId.is(aliasId), sId.is(shareId))
Sql.and(aId.is(aliasId), uEmail.isNotNull)
)
.query[(RAlias, RAccount)]
.option
}

(for {
aliasInfo <- OptionT(baseQuery)
users <- OptionT.liftF(
(memberQuery ++ fr"UNION ALL" ++ ownerQuery)
.query[NotifyData.AccountInfo]
.to[List]
)
} yield NotifyData(aliasInfo._1, aliasInfo._2, users)).value
}

def publishIdAndPassword(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -645,7 +645,7 @@ object OShare {
accId,
alias,
data.name,
dbalias.map(_.validity).getOrElse(data.validity),
dbalias.map(_._1.validity).getOrElse(data.validity),
data.maxViews,
data.password,
data.description,
Expand Down
Loading

0 comments on commit d47ba33

Please sign in to comment.