mirror of
https://github.com/TheAnachronism/docspell.git
synced 2025-04-05 10:59:33 +00:00
commit
e27e76d6fb
@ -445,7 +445,8 @@ val joex = project
|
||||
buildInfoPackage := "docspell.joex",
|
||||
reStart / javaOptions ++= Seq(
|
||||
s"-Dconfig.file=${(LocalRootProject / baseDirectory).value / "local" / "dev.conf"}"
|
||||
)
|
||||
),
|
||||
Revolver.enableDebugging(port = 5051, suspend = false)
|
||||
)
|
||||
.dependsOn(store, backend, extract, convert, analysis, joexapi, restapi, ftssolr)
|
||||
|
||||
@ -492,7 +493,8 @@ val restserver = project
|
||||
),
|
||||
reStart / javaOptions ++= Seq(
|
||||
s"-Dconfig.file=${(LocalRootProject / baseDirectory).value / "local" / "dev.conf"}"
|
||||
)
|
||||
),
|
||||
Revolver.enableDebugging(port = 5050, suspend = false)
|
||||
)
|
||||
.dependsOn(restapi, joexapi, backend, webapp, ftssolr)
|
||||
|
||||
|
@ -66,8 +66,8 @@ trait OCollective[F[_]] {
|
||||
|
||||
object OCollective {
|
||||
|
||||
type TagCount = QCollective.TagCount
|
||||
val TagCount = QCollective.TagCount
|
||||
type TagCount = docspell.store.queries.TagCount
|
||||
val TagCount = docspell.store.queries.TagCount
|
||||
|
||||
type InsightData = QCollective.InsightData
|
||||
val insightData = QCollective.InsightData
|
||||
|
@ -7,33 +7,39 @@ import fs2.Stream
|
||||
import docspell.backend.JobFactory
|
||||
import docspell.backend.ops.OItemSearch._
|
||||
import docspell.common._
|
||||
import docspell.common.syntax.all._
|
||||
import docspell.ftsclient._
|
||||
import docspell.store.Store
|
||||
import docspell.store.queries.{QFolder, QItem}
|
||||
import docspell.store.queries.{QFolder, QItem, SelectedItem}
|
||||
import docspell.store.queue.JobQueue
|
||||
import docspell.store.records.RJob
|
||||
import docspell.store.{Store, qb}
|
||||
|
||||
import org.log4s.getLogger
|
||||
|
||||
trait OFulltext[F[_]] {
|
||||
|
||||
def findItems(maxNoteLen: Int)(
|
||||
q: Query,
|
||||
fts: OFulltext.FtsInput,
|
||||
batch: Batch
|
||||
batch: qb.Batch
|
||||
): F[Vector[OFulltext.FtsItem]]
|
||||
|
||||
/** Same as `findItems` but does more queries per item to find all tags. */
|
||||
def findItemsWithTags(maxNoteLen: Int)(
|
||||
q: Query,
|
||||
fts: OFulltext.FtsInput,
|
||||
batch: Batch
|
||||
batch: qb.Batch
|
||||
): F[Vector[OFulltext.FtsItemWithTags]]
|
||||
|
||||
def findIndexOnly(maxNoteLen: Int)(
|
||||
fts: OFulltext.FtsInput,
|
||||
account: AccountId,
|
||||
batch: Batch
|
||||
batch: qb.Batch
|
||||
): F[Vector[OFulltext.FtsItemWithTags]]
|
||||
|
||||
def findIndexOnlySummary(account: AccountId, fts: OFulltext.FtsInput): F[SearchSummary]
|
||||
def findItemsSummary(q: Query, fts: OFulltext.FtsInput): F[SearchSummary]
|
||||
|
||||
/** Clears the full-text index completely and launches a task that
|
||||
* indexes all data.
|
||||
*/
|
||||
@ -46,6 +52,7 @@ trait OFulltext[F[_]] {
|
||||
}
|
||||
|
||||
object OFulltext {
|
||||
private[this] val logger = getLogger
|
||||
|
||||
case class FtsInput(
|
||||
query: String,
|
||||
@ -77,12 +84,14 @@ object OFulltext {
|
||||
Resource.pure[F, OFulltext[F]](new OFulltext[F] {
|
||||
def reindexAll: F[Unit] =
|
||||
for {
|
||||
_ <- logger.finfo(s"Re-index all.")
|
||||
job <- JobFactory.reIndexAll[F]
|
||||
_ <- queue.insertIfNew(job) *> joex.notifyAllNodes
|
||||
} yield ()
|
||||
|
||||
def reindexCollective(account: AccountId): F[Unit] =
|
||||
for {
|
||||
_ <- logger.fdebug(s"Re-index collective: $account")
|
||||
exist <- store.transact(
|
||||
RJob.findNonFinalByTracker(DocspellSystem.migrationTaskTracker)
|
||||
)
|
||||
@ -95,7 +104,7 @@ object OFulltext {
|
||||
def findIndexOnly(maxNoteLen: Int)(
|
||||
ftsQ: OFulltext.FtsInput,
|
||||
account: AccountId,
|
||||
batch: Batch
|
||||
batch: qb.Batch
|
||||
): F[Vector[OFulltext.FtsItemWithTags]] = {
|
||||
val fq = FtsQuery(
|
||||
ftsQ.query,
|
||||
@ -107,20 +116,21 @@ object OFulltext {
|
||||
FtsQuery.HighlightSetting(ftsQ.highlightPre, ftsQ.highlightPost)
|
||||
)
|
||||
for {
|
||||
_ <- logger.ftrace(s"Find index only: ${ftsQ.query}/${batch}")
|
||||
folders <- store.transact(QFolder.getMemberFolders(account))
|
||||
ftsR <- fts.search(fq.withFolders(folders))
|
||||
ftsItems = ftsR.results.groupBy(_.itemId)
|
||||
select =
|
||||
ftsItems.values
|
||||
.map(_.sortBy(-_.score).head)
|
||||
.map(r => QItem.SelectedItem(r.itemId, r.score))
|
||||
.map(_.minBy(-_.score))
|
||||
.map(r => SelectedItem(r.itemId, r.score))
|
||||
.toSet
|
||||
itemsWithTags <-
|
||||
store
|
||||
.transact(
|
||||
QItem.findItemsWithTags(
|
||||
account.collective,
|
||||
QItem.findSelectedItems(QItem.Query.empty(account), maxNoteLen, select)
|
||||
QItem.findSelectedItems(Query.empty(account), maxNoteLen, select)
|
||||
)
|
||||
)
|
||||
.take(batch.limit.toLong)
|
||||
@ -133,9 +143,35 @@ object OFulltext {
|
||||
} yield res
|
||||
}
|
||||
|
||||
def findIndexOnlySummary(
|
||||
account: AccountId,
|
||||
ftsQ: OFulltext.FtsInput
|
||||
): F[SearchSummary] = {
|
||||
val fq = FtsQuery(
|
||||
ftsQ.query,
|
||||
account.collective,
|
||||
Set.empty,
|
||||
Set.empty,
|
||||
500,
|
||||
0,
|
||||
FtsQuery.HighlightSetting.default
|
||||
)
|
||||
|
||||
for {
|
||||
folder <- store.transact(QFolder.getMemberFolders(account))
|
||||
itemIds <- fts
|
||||
.searchAll(fq.withFolders(folder))
|
||||
.flatMap(r => Stream.emits(r.results.map(_.itemId)))
|
||||
.compile
|
||||
.to(Set)
|
||||
q = Query.empty(account).copy(itemIds = itemIds.some)
|
||||
res <- store.transact(QItem.searchStats(q))
|
||||
} yield res
|
||||
}
|
||||
|
||||
def findItems(
|
||||
maxNoteLen: Int
|
||||
)(q: Query, ftsQ: FtsInput, batch: Batch): F[Vector[FtsItem]] =
|
||||
)(q: Query, ftsQ: FtsInput, batch: qb.Batch): F[Vector[FtsItem]] =
|
||||
findItemsFts(
|
||||
q,
|
||||
ftsQ,
|
||||
@ -152,7 +188,7 @@ object OFulltext {
|
||||
def findItemsWithTags(maxNoteLen: Int)(
|
||||
q: Query,
|
||||
ftsQ: FtsInput,
|
||||
batch: Batch
|
||||
batch: qb.Batch
|
||||
): F[Vector[FtsItemWithTags]] =
|
||||
findItemsFts(
|
||||
q,
|
||||
@ -167,13 +203,34 @@ object OFulltext {
|
||||
.compile
|
||||
.toVector
|
||||
|
||||
def findItemsSummary(q: Query, ftsQ: OFulltext.FtsInput): F[SearchSummary] =
|
||||
for {
|
||||
search <- itemSearch.findItems(0)(q, Batch.all)
|
||||
fq = FtsQuery(
|
||||
ftsQ.query,
|
||||
q.account.collective,
|
||||
search.map(_.id).toSet,
|
||||
Set.empty,
|
||||
500,
|
||||
0,
|
||||
FtsQuery.HighlightSetting.default
|
||||
)
|
||||
items <- fts
|
||||
.searchAll(fq)
|
||||
.flatMap(r => Stream.emits(r.results.map(_.itemId)))
|
||||
.compile
|
||||
.to(Set)
|
||||
qnext = q.copy(itemIds = items.some)
|
||||
res <- store.transact(QItem.searchStats(qnext))
|
||||
} yield res
|
||||
|
||||
// Helper
|
||||
|
||||
private def findItemsFts[A: ItemId, B](
|
||||
q: Query,
|
||||
ftsQ: FtsInput,
|
||||
batch: Batch,
|
||||
search: (Query, Batch) => F[Vector[A]],
|
||||
batch: qb.Batch,
|
||||
search: (Query, qb.Batch) => F[Vector[A]],
|
||||
convert: (
|
||||
FtsResult,
|
||||
Map[Ident, List[FtsResult.ItemMatch]]
|
||||
@ -186,8 +243,8 @@ object OFulltext {
|
||||
private def findItemsFts0[A: ItemId, B](
|
||||
q: Query,
|
||||
ftsQ: FtsInput,
|
||||
batch: Batch,
|
||||
search: (Query, Batch) => F[Vector[A]],
|
||||
batch: qb.Batch,
|
||||
search: (Query, qb.Batch) => F[Vector[A]],
|
||||
convert: (
|
||||
FtsResult,
|
||||
Map[Ident, List[FtsResult.ItemMatch]]
|
||||
@ -227,10 +284,9 @@ object OFulltext {
|
||||
): PartialFunction[A, (A, FtsData)] = {
|
||||
case a if ftrItems.contains(ItemId[A].itemId(a)) =>
|
||||
val ftsDataItems = ftrItems
|
||||
.get(ItemId[A].itemId(a))
|
||||
.getOrElse(Nil)
|
||||
.getOrElse(ItemId[A].itemId(a), Nil)
|
||||
.map(im =>
|
||||
FtsDataItem(im.score, im.data, ftr.highlight.get(im.id).getOrElse(Nil))
|
||||
FtsDataItem(im.score, im.data, ftr.highlight.getOrElse(im.id, Nil))
|
||||
)
|
||||
(a, FtsData(ftr.maxScore, ftr.count, ftr.qtime, ftsDataItems))
|
||||
}
|
||||
|
@ -9,7 +9,7 @@ import docspell.backend.JobFactory
|
||||
import docspell.common._
|
||||
import docspell.ftsclient.FtsClient
|
||||
import docspell.store.UpdateResult
|
||||
import docspell.store.queries.{QAttachment, QItem}
|
||||
import docspell.store.queries.{QAttachment, QItem, QMoveAttachment}
|
||||
import docspell.store.queue.JobQueue
|
||||
import docspell.store.records._
|
||||
import docspell.store.{AddResult, Store}
|
||||
@ -206,7 +206,7 @@ object OItem {
|
||||
target: Ident
|
||||
): F[AddResult] =
|
||||
store
|
||||
.transact(QItem.moveAttachmentBefore(itemId, source, target))
|
||||
.transact(QMoveAttachment.moveAttachmentBefore(itemId, source, target))
|
||||
.attempt
|
||||
.map(AddResult.fromUpdate)
|
||||
|
||||
|
@ -7,7 +7,7 @@ import fs2.Stream
|
||||
|
||||
import docspell.backend.ops.OItemSearch._
|
||||
import docspell.common._
|
||||
import docspell.store.Store
|
||||
import docspell.store._
|
||||
import docspell.store.queries.{QAttachment, QItem}
|
||||
import docspell.store.records._
|
||||
|
||||
@ -24,6 +24,8 @@ trait OItemSearch[F[_]] {
|
||||
maxNoteLen: Int
|
||||
)(q: Query, batch: Batch): F[Vector[ListItemWithTags]]
|
||||
|
||||
def findItemsSummary(q: Query): F[SearchSummary]
|
||||
|
||||
def findAttachment(id: Ident, collective: Ident): F[Option[AttachmentData[F]]]
|
||||
|
||||
def findAttachmentSource(
|
||||
@ -53,26 +55,29 @@ trait OItemSearch[F[_]] {
|
||||
|
||||
object OItemSearch {
|
||||
|
||||
type CustomValue = QItem.CustomValue
|
||||
val CustomValue = QItem.CustomValue
|
||||
type SearchSummary = queries.SearchSummary
|
||||
val SearchSummary = queries.SearchSummary
|
||||
|
||||
type Query = QItem.Query
|
||||
val Query = QItem.Query
|
||||
type CustomValue = queries.CustomValue
|
||||
val CustomValue = queries.CustomValue
|
||||
|
||||
type Batch = QItem.Batch
|
||||
val Batch = QItem.Batch
|
||||
type Query = queries.Query
|
||||
val Query = queries.Query
|
||||
|
||||
type ListItem = QItem.ListItem
|
||||
val ListItem = QItem.ListItem
|
||||
type Batch = qb.Batch
|
||||
val Batch = docspell.store.qb.Batch
|
||||
|
||||
type ListItemWithTags = QItem.ListItemWithTags
|
||||
val ListItemWithTags = QItem.ListItemWithTags
|
||||
type ListItem = queries.ListItem
|
||||
val ListItem = queries.ListItem
|
||||
|
||||
type ItemFieldValue = QItem.ItemFieldValue
|
||||
val ItemFieldValue = QItem.ItemFieldValue
|
||||
type ListItemWithTags = queries.ListItemWithTags
|
||||
val ListItemWithTags = queries.ListItemWithTags
|
||||
|
||||
type ItemData = QItem.ItemData
|
||||
val ItemData = QItem.ItemData
|
||||
type ItemFieldValue = queries.ItemFieldValue
|
||||
val ItemFieldValue = queries.ItemFieldValue
|
||||
|
||||
type ItemData = queries.ItemData
|
||||
val ItemData = queries.ItemData
|
||||
|
||||
trait BinaryData[F[_]] {
|
||||
def data: Stream[F, Byte]
|
||||
@ -139,6 +144,9 @@ object OItemSearch {
|
||||
.toVector
|
||||
}
|
||||
|
||||
def findItemsSummary(q: Query): F[SearchSummary] =
|
||||
store.transact(QItem.searchStats(q))
|
||||
|
||||
def findAttachment(id: Ident, collective: Ident): F[Option[AttachmentData[F]]] =
|
||||
store
|
||||
.transact(RAttachment.findByIdAndCollective(id, collective))
|
||||
|
@ -39,7 +39,7 @@ object OJob {
|
||||
def queued: Vector[JobDetail] =
|
||||
jobs.filter(r => JobState.queued.contains(r.job.state))
|
||||
def done: Vector[JobDetail] =
|
||||
jobs.filter(r => JobState.done.contains(r.job.state))
|
||||
jobs.filter(r => JobState.done.toList.contains(r.job.state))
|
||||
def running: Vector[JobDetail] =
|
||||
jobs.filter(_.job.state == JobState.Running)
|
||||
}
|
||||
|
@ -2,6 +2,7 @@ package docspell.common
|
||||
|
||||
import java.time.LocalDate
|
||||
|
||||
import cats.data.NonEmptyList
|
||||
import cats.implicits._
|
||||
|
||||
import io.circe._
|
||||
@ -92,7 +93,8 @@ object CustomFieldType {
|
||||
def bool: CustomFieldType = Bool
|
||||
def money: CustomFieldType = Money
|
||||
|
||||
val all: List[CustomFieldType] = List(Text, Numeric, Date, Bool, Money)
|
||||
val all: NonEmptyList[CustomFieldType] =
|
||||
NonEmptyList.of(Text, Numeric, Date, Bool, Money)
|
||||
|
||||
def fromString(str: String): Either[String, CustomFieldType] =
|
||||
str.toLowerCase match {
|
||||
|
@ -1,5 +1,7 @@
|
||||
package docspell.common
|
||||
|
||||
import cats.data.NonEmptyList
|
||||
|
||||
import io.circe.{Decoder, Encoder}
|
||||
|
||||
sealed trait JobState { self: Product =>
|
||||
@ -12,8 +14,6 @@ object JobState {
|
||||
/** Waiting for being executed. */
|
||||
case object Waiting extends JobState {}
|
||||
|
||||
def waiting: JobState = Waiting
|
||||
|
||||
/** A scheduler has picked up this job and will pass it to the next
|
||||
* free slot.
|
||||
*/
|
||||
@ -34,10 +34,20 @@ object JobState {
|
||||
/** Finished with success */
|
||||
case object Success extends JobState {}
|
||||
|
||||
val all: Set[JobState] =
|
||||
Set(Waiting, Scheduled, Running, Stuck, Failed, Cancelled, Success)
|
||||
val waiting: JobState = Waiting
|
||||
val stuck: JobState = Stuck
|
||||
val scheduled: JobState = Scheduled
|
||||
val running: JobState = Running
|
||||
val failed: JobState = Failed
|
||||
val cancelled: JobState = Cancelled
|
||||
val success: JobState = Success
|
||||
|
||||
val all: NonEmptyList[JobState] =
|
||||
NonEmptyList.of(Waiting, Scheduled, Running, Stuck, Failed, Cancelled, Success)
|
||||
val queued: Set[JobState] = Set(Waiting, Scheduled, Stuck)
|
||||
val done: Set[JobState] = Set(Failed, Cancelled, Success)
|
||||
val done: NonEmptyList[JobState] = NonEmptyList.of(Failed, Cancelled, Success)
|
||||
val notDone: NonEmptyList[JobState] = //all - done
|
||||
NonEmptyList.of(Waiting, Scheduled, Running, Stuck)
|
||||
val inProgress: Set[JobState] = Set(Scheduled, Running, Stuck)
|
||||
|
||||
def parse(str: String): Either[String, JobState] =
|
||||
|
@ -136,27 +136,21 @@ object RegexNerFile {
|
||||
|
||||
object Sql {
|
||||
import doobie._
|
||||
import doobie.implicits._
|
||||
import docspell.store.impl.Implicits._
|
||||
import docspell.store.impl.Column
|
||||
import docspell.store.qb.DSL._
|
||||
import docspell.store.qb._
|
||||
|
||||
def latestUpdate(collective: Ident): ConnectionIO[Option[Timestamp]] = {
|
||||
def max(col: Column, table: Fragment, cidCol: Column): Fragment =
|
||||
selectSimple(col.max ++ fr"as t", table, cidCol.is(collective))
|
||||
def max_(col: Column[_], cidCol: Column[Ident]): Select =
|
||||
Select(max(col).as("t"), from(col.table), cidCol === collective)
|
||||
|
||||
val sql =
|
||||
List(
|
||||
max(
|
||||
ROrganization.Columns.updated,
|
||||
ROrganization.table,
|
||||
ROrganization.Columns.cid
|
||||
),
|
||||
max(RPerson.Columns.updated, RPerson.table, RPerson.Columns.cid),
|
||||
max(REquipment.Columns.updated, REquipment.table, REquipment.Columns.cid)
|
||||
val sql = union(
|
||||
max_(ROrganization.T.updated, ROrganization.T.cid),
|
||||
max_(RPerson.T.updated, RPerson.T.cid),
|
||||
max_(REquipment.T.updated, REquipment.T.cid)
|
||||
)
|
||||
.reduce(_ ++ fr"UNION ALL" ++ _)
|
||||
val t = Column[Timestamp]("t", TableDef(""))
|
||||
|
||||
selectSimple(fr"MAX(t)", fr"(" ++ sql ++ fr") as x", Fragment.empty)
|
||||
run(select(max(t)), from(sql, "x"))
|
||||
.query[Option[Timestamp]]
|
||||
.option
|
||||
.map(_.flatten)
|
||||
|
@ -2,7 +2,7 @@ package docspell.joex.notify
|
||||
|
||||
import docspell.common._
|
||||
import docspell.joex.notify.YamuscaConverter._
|
||||
import docspell.store.queries.QItem
|
||||
import docspell.store.queries.ListItem
|
||||
|
||||
import yamusca.implicits._
|
||||
import yamusca.imports._
|
||||
@ -19,7 +19,7 @@ case class MailContext(
|
||||
object MailContext {
|
||||
|
||||
def from(
|
||||
items: Vector[QItem.ListItem],
|
||||
items: Vector[ListItem],
|
||||
max: Int,
|
||||
account: AccountId,
|
||||
itemBaseUri: Option[LenientUri],
|
||||
@ -46,7 +46,7 @@ object MailContext {
|
||||
|
||||
object ItemData {
|
||||
|
||||
def apply(now: Timestamp)(i: QItem.ListItem): ItemData = {
|
||||
def apply(now: Timestamp)(i: ListItem): ItemData = {
|
||||
val dueIn = i.dueDate.map(dt => Timestamp.daysBetween(now, dt))
|
||||
val dueInLabel = dueIn.map {
|
||||
case 0 => "**today**"
|
||||
|
@ -4,7 +4,7 @@ import cats.data.OptionT
|
||||
import cats.effect._
|
||||
import cats.implicits._
|
||||
|
||||
import docspell.backend.ops.OItemSearch.Batch
|
||||
import docspell.backend.ops.OItemSearch.{Batch, ListItem, Query}
|
||||
import docspell.common._
|
||||
import docspell.joex.mail.EmilHeader
|
||||
import docspell.joex.scheduler.{Context, Task}
|
||||
@ -66,11 +66,11 @@ object NotifyDueItemsTask {
|
||||
mail <- OptionT.liftF(makeMail(sendCfg, cfg, ctx.args, items))
|
||||
} yield mail
|
||||
|
||||
def findItems[F[_]: Sync](ctx: Context[F, Args]): F[Vector[QItem.ListItem]] =
|
||||
def findItems[F[_]: Sync](ctx: Context[F, Args]): F[Vector[ListItem]] =
|
||||
for {
|
||||
now <- Timestamp.current[F]
|
||||
q =
|
||||
QItem.Query
|
||||
Query
|
||||
.empty(ctx.args.account)
|
||||
.copy(
|
||||
states = ItemState.validStates.toList,
|
||||
@ -91,7 +91,7 @@ object NotifyDueItemsTask {
|
||||
sendCfg: MailSendConfig,
|
||||
cfg: RUserEmail,
|
||||
args: Args,
|
||||
items: Vector[QItem.ListItem]
|
||||
items: Vector[ListItem]
|
||||
): F[Mail[F]] =
|
||||
Timestamp.current[F].map { now =>
|
||||
val templateCtx =
|
||||
|
@ -121,7 +121,7 @@ object CreateItem {
|
||||
|
||||
private def findExisting[F[_]: Sync]: Task[F, ProcessItemArgs, Option[ItemData]] =
|
||||
Task { ctx =>
|
||||
val states = ItemState.invalidStates.toList.toSet
|
||||
val states = ItemState.invalidStates
|
||||
val fileMetaIds = ctx.args.files.map(_.fileMetaId).toSet
|
||||
for {
|
||||
cand <- ctx.store.transact(QItem.findByFileIds(fileMetaIds.toSeq, states))
|
||||
|
@ -105,7 +105,7 @@ object ItemHandler {
|
||||
|
||||
private def deleteByFileIds[F[_]: Sync: ContextShift]: Task[F, Args, Unit] =
|
||||
Task { ctx =>
|
||||
val states = ItemState.invalidStates.toList.toSet
|
||||
val states = ItemState.invalidStates
|
||||
for {
|
||||
items <- ctx.store.transact(
|
||||
QItem.findByFileIds(ctx.args.files.map(_.fileMetaId), states)
|
||||
|
@ -1375,6 +1375,27 @@ paths:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ItemLightList"
|
||||
|
||||
/sec/item/searchStats:
|
||||
post:
|
||||
tags: [ Item ]
|
||||
summary: Get basic statistics about the data of a search.
|
||||
description: |
|
||||
Takes a search query and returns a summary about the results.
|
||||
security:
|
||||
- authTokenHeader: []
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ItemSearch"
|
||||
responses:
|
||||
200:
|
||||
description: Ok
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/SearchStats"
|
||||
|
||||
/sec/item/{id}:
|
||||
get:
|
||||
tags: [ Item ]
|
||||
@ -4146,6 +4167,28 @@ components:
|
||||
key:
|
||||
type: string
|
||||
format: ident
|
||||
SearchStats:
|
||||
description: |
|
||||
A summary of search results.
|
||||
required:
|
||||
- count
|
||||
- tagCloud
|
||||
- fieldStats
|
||||
- folderStats
|
||||
properties:
|
||||
count:
|
||||
type: integer
|
||||
format: int32
|
||||
tagCloud:
|
||||
$ref: "#/components/schemas/TagCloud"
|
||||
fieldStats:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/FieldStats"
|
||||
folderStats:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/FolderStats"
|
||||
ItemInsights:
|
||||
description: |
|
||||
Information about the items in docspell.
|
||||
@ -4166,6 +4209,70 @@ components:
|
||||
format: int64
|
||||
tagCloud:
|
||||
$ref: "#/components/schemas/TagCloud"
|
||||
FolderStats:
|
||||
description: |
|
||||
Count of folder usage.
|
||||
required:
|
||||
- id
|
||||
- name
|
||||
- owner
|
||||
- count
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
format: ident
|
||||
name:
|
||||
type: string
|
||||
owner:
|
||||
$ref: "#/components/schemas/IdName"
|
||||
count:
|
||||
type: integer
|
||||
format: int32
|
||||
FieldStats:
|
||||
description: |
|
||||
Basic statistics about a custom field.
|
||||
required:
|
||||
- id
|
||||
- name
|
||||
- ftype
|
||||
- count
|
||||
- avg
|
||||
- sum
|
||||
- max
|
||||
- min
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
format: ident
|
||||
name:
|
||||
type: string
|
||||
format: ident
|
||||
label:
|
||||
type: string
|
||||
ftype:
|
||||
type: string
|
||||
format: customfieldtype
|
||||
enum:
|
||||
- text
|
||||
- numeric
|
||||
- date
|
||||
- bool
|
||||
- money
|
||||
count:
|
||||
type: integer
|
||||
format: int32
|
||||
sum:
|
||||
type: number
|
||||
format: double
|
||||
avg:
|
||||
type: number
|
||||
format: double
|
||||
max:
|
||||
type: number
|
||||
format: double
|
||||
min:
|
||||
type: number
|
||||
format: double
|
||||
TagCloud:
|
||||
description: |
|
||||
A tag "cloud"
|
||||
@ -5079,7 +5186,6 @@ components:
|
||||
- state
|
||||
- date
|
||||
- source
|
||||
- fileCount
|
||||
- tags
|
||||
properties:
|
||||
id:
|
||||
@ -5113,9 +5219,6 @@ components:
|
||||
$ref: "#/components/schemas/IdName"
|
||||
folder:
|
||||
$ref: "#/components/schemas/IdName"
|
||||
fileCount:
|
||||
type: integer
|
||||
format: int32
|
||||
attachments:
|
||||
type: array
|
||||
items:
|
||||
|
@ -16,7 +16,7 @@ import docspell.common.syntax.all._
|
||||
import docspell.ftsclient.FtsResult
|
||||
import docspell.restapi.model._
|
||||
import docspell.restserver.conv.Conversions._
|
||||
import docspell.store.queries.QItem
|
||||
import docspell.store.queries.{AttachmentLight => QAttachmentLight}
|
||||
import docspell.store.records._
|
||||
import docspell.store.{AddResult, UpdateResult}
|
||||
|
||||
@ -27,6 +27,30 @@ import org.log4s.Logger
|
||||
|
||||
trait Conversions {
|
||||
|
||||
def mkSearchStats(sum: OItemSearch.SearchSummary): SearchStats =
|
||||
SearchStats(
|
||||
sum.count,
|
||||
mkTagCloud(sum.tags),
|
||||
sum.fields.map(mkFieldStats),
|
||||
sum.folders.map(mkFolderStats)
|
||||
)
|
||||
|
||||
def mkFolderStats(fs: docspell.store.queries.FolderCount): FolderStats =
|
||||
FolderStats(fs.id, fs.name, mkIdName(fs.owner), fs.count)
|
||||
|
||||
def mkFieldStats(fs: docspell.store.queries.FieldStats): FieldStats =
|
||||
FieldStats(
|
||||
fs.field.id,
|
||||
fs.field.name,
|
||||
fs.field.label,
|
||||
fs.field.ftype,
|
||||
fs.count,
|
||||
fs.sum.doubleValue,
|
||||
fs.avg.doubleValue,
|
||||
fs.max.doubleValue,
|
||||
fs.min.doubleValue
|
||||
)
|
||||
|
||||
// insights
|
||||
def mkItemInsights(d: InsightData): ItemInsights =
|
||||
ItemInsights(
|
||||
@ -213,7 +237,6 @@ trait Conversions {
|
||||
i.concPerson.map(mkIdName),
|
||||
i.concEquip.map(mkIdName),
|
||||
i.folder.map(mkIdName),
|
||||
i.fileCount,
|
||||
Nil, //attachments
|
||||
Nil, //tags
|
||||
Nil, //customfields
|
||||
@ -235,7 +258,7 @@ trait Conversions {
|
||||
customfields = i.customfields.map(mkItemFieldValue)
|
||||
)
|
||||
|
||||
private def mkAttachmentLight(qa: QItem.AttachmentLight): AttachmentLight =
|
||||
private def mkAttachmentLight(qa: QAttachmentLight): AttachmentLight =
|
||||
AttachmentLight(qa.id, qa.position, qa.name, qa.pageCount)
|
||||
|
||||
def mkItemLightWithTags(i: OFulltext.FtsItemWithTags): ItemLight = {
|
||||
|
@ -1,17 +1,17 @@
|
||||
package docspell.restserver.routes
|
||||
|
||||
import cats.Monoid
|
||||
import cats.data.NonEmptyList
|
||||
import cats.effect._
|
||||
import cats.implicits._
|
||||
import cats.Monoid
|
||||
|
||||
import docspell.backend.BackendApp
|
||||
import docspell.backend.auth.AuthToken
|
||||
import docspell.backend.ops.OCustomFields.{RemoveValue, SetValue}
|
||||
import docspell.backend.ops.OFulltext
|
||||
import docspell.backend.ops.OItemSearch.Batch
|
||||
import docspell.common.syntax.all._
|
||||
import docspell.common._
|
||||
import docspell.common.syntax.all._
|
||||
import docspell.restapi.model._
|
||||
import docspell.restserver.Config
|
||||
import docspell.restserver.conv.Conversions
|
||||
@ -143,6 +143,25 @@ object ItemRoutes {
|
||||
}
|
||||
} yield resp
|
||||
|
||||
case req @ POST -> Root / "searchStats" =>
|
||||
for {
|
||||
mask <- req.as[ItemSearch]
|
||||
query = Conversions.mkQuery(mask, user.account)
|
||||
stats <- mask match {
|
||||
case SearchFulltextOnly(ftq) if cfg.fullTextSearch.enabled =>
|
||||
logger.finfo(s"Make index only summary: $ftq") *>
|
||||
backend.fulltext.findIndexOnlySummary(
|
||||
user.account,
|
||||
OFulltext.FtsInput(ftq.query)
|
||||
)
|
||||
case SearchWithFulltext(fq) if cfg.fullTextSearch.enabled =>
|
||||
backend.fulltext.findItemsSummary(query, OFulltext.FtsInput(fq))
|
||||
case _ =>
|
||||
backend.itemSearch.findItemsSummary(query)
|
||||
}
|
||||
resp <- Ok(Conversions.mkSearchStats(stats))
|
||||
} yield resp
|
||||
|
||||
case GET -> Root / Ident(id) =>
|
||||
for {
|
||||
item <- backend.itemSearch.findItem(id, user.account.collective)
|
||||
|
@ -1,139 +0,0 @@
|
||||
package docspell.store.impl
|
||||
|
||||
import cats.data.NonEmptyList
|
||||
|
||||
import docspell.store.impl.DoobieSyntax._
|
||||
|
||||
import doobie._
|
||||
import doobie.implicits._
|
||||
|
||||
case class Column(name: String, ns: String = "", alias: String = "") {
|
||||
|
||||
val f = {
|
||||
val col =
|
||||
if (ns.isEmpty) Fragment.const(name)
|
||||
else Fragment.const(ns + "." + name)
|
||||
if (alias.isEmpty) col
|
||||
else col ++ fr"as" ++ Fragment.const(alias)
|
||||
}
|
||||
|
||||
def lowerLike[A: Put](value: A): Fragment =
|
||||
fr"lower(" ++ f ++ fr") LIKE $value"
|
||||
|
||||
def like[A: Put](value: A): Fragment =
|
||||
f ++ fr"LIKE $value"
|
||||
|
||||
def is[A: Put](value: A): Fragment =
|
||||
f ++ fr" = $value"
|
||||
|
||||
def lowerIs[A: Put](value: A): Fragment =
|
||||
fr"lower(" ++ f ++ fr") = $value"
|
||||
|
||||
def is[A: Put](ov: Option[A]): Fragment =
|
||||
ov match {
|
||||
case Some(v) => f ++ fr" = $v"
|
||||
case None => f ++ fr"is null"
|
||||
}
|
||||
|
||||
def is(c: Column): Fragment =
|
||||
f ++ fr"=" ++ c.f
|
||||
|
||||
def isSubquery(sq: Fragment): Fragment =
|
||||
f ++ fr"=" ++ fr"(" ++ sq ++ fr")"
|
||||
|
||||
def isNot[A: Put](value: A): Fragment =
|
||||
f ++ fr"<> $value"
|
||||
|
||||
def isNot(c: Column): Fragment =
|
||||
f ++ fr"<>" ++ c.f
|
||||
|
||||
def isNull: Fragment =
|
||||
f ++ fr"is null"
|
||||
|
||||
def isNotNull: Fragment =
|
||||
f ++ fr"is not null"
|
||||
|
||||
def isIn(values: Seq[Fragment]): Fragment =
|
||||
f ++ fr"IN (" ++ commas(values) ++ fr")"
|
||||
|
||||
def isIn[A: Put](values: NonEmptyList[A]): Fragment =
|
||||
values.tail match {
|
||||
case Nil =>
|
||||
is(values.head)
|
||||
case _ =>
|
||||
isIn(values.map(a => sql"$a").toList)
|
||||
}
|
||||
|
||||
def isLowerIn[A: Put](values: NonEmptyList[A]): Fragment =
|
||||
fr"lower(" ++ f ++ fr") IN (" ++ commas(values.map(a => sql"$a").toList) ++ fr")"
|
||||
|
||||
def isIn(frag: Fragment): Fragment =
|
||||
f ++ fr"IN (" ++ frag ++ fr")"
|
||||
|
||||
def isOrDiscard[A: Put](value: Option[A]): Fragment =
|
||||
value match {
|
||||
case Some(v) => is(v)
|
||||
case None => Fragment.empty
|
||||
}
|
||||
|
||||
def isOneOf[A: Put](values: Seq[A]): Fragment = {
|
||||
val vals = values.map(v => sql"$v")
|
||||
isIn(vals)
|
||||
}
|
||||
|
||||
def isNotOneOf[A: Put](values: Seq[A]): Fragment = {
|
||||
val vals = values.map(v => sql"$v")
|
||||
sql"(" ++ f ++ fr"is null or" ++ f ++ fr"not IN (" ++ commas(vals) ++ sql"))"
|
||||
}
|
||||
|
||||
def isGt[A: Put](a: A): Fragment =
|
||||
f ++ fr"> $a"
|
||||
|
||||
def isGte[A: Put](a: A): Fragment =
|
||||
f ++ fr">= $a"
|
||||
|
||||
def isGt(c: Column): Fragment =
|
||||
f ++ fr">" ++ c.f
|
||||
|
||||
def isLt[A: Put](a: A): Fragment =
|
||||
f ++ fr"< $a"
|
||||
|
||||
def isLte[A: Put](a: A): Fragment =
|
||||
f ++ fr"<= $a"
|
||||
|
||||
def isLt(c: Column): Fragment =
|
||||
f ++ fr"<" ++ c.f
|
||||
|
||||
def setTo[A: Put](value: A): Fragment =
|
||||
is(value)
|
||||
|
||||
def setTo[A: Put](va: Option[A]): Fragment =
|
||||
f ++ fr" = $va"
|
||||
|
||||
def ++(next: Fragment): Fragment =
|
||||
f.++(next)
|
||||
|
||||
def prefix(ns: String): Column =
|
||||
Column(name, ns)
|
||||
|
||||
def as(alias: String): Column =
|
||||
Column(name, ns, alias)
|
||||
|
||||
def desc: Fragment =
|
||||
f ++ fr"desc"
|
||||
def asc: Fragment =
|
||||
f ++ fr"asc"
|
||||
|
||||
def max: Fragment =
|
||||
fr"MAX(" ++ f ++ fr")"
|
||||
|
||||
def increment[A: Put](a: A): Fragment =
|
||||
f ++ fr"=" ++ f ++ fr"+ $a"
|
||||
|
||||
def decrement[A: Put](a: A): Fragment =
|
||||
f ++ fr"=" ++ f ++ fr"- $a"
|
||||
|
||||
def substring(from: Int, many: Int): Fragment =
|
||||
if (many <= 0 || from < 0) fr"${""}"
|
||||
else fr"SUBSTRING(" ++ f ++ fr"FROM $from FOR $many)"
|
||||
}
|
@ -1,103 +0,0 @@
|
||||
package docspell.store.impl
|
||||
|
||||
import cats.data.NonEmptyList
|
||||
|
||||
import docspell.common.Timestamp
|
||||
|
||||
import doobie._
|
||||
import doobie.implicits._
|
||||
|
||||
trait DoobieSyntax {
|
||||
|
||||
def groupBy(c0: Column, cs: Column*): Fragment =
|
||||
groupBy(NonEmptyList.of(c0, cs: _*))
|
||||
|
||||
def groupBy(cs: NonEmptyList[Column]): Fragment =
|
||||
fr" GROUP BY " ++ commas(cs.toList.map(_.f))
|
||||
|
||||
def coalesce(f0: Fragment, fs: Fragment*): Fragment =
|
||||
sql" coalesce(" ++ commas(f0 :: fs.toList) ++ sql") "
|
||||
|
||||
def power2(c: Column): Fragment =
|
||||
sql"power(2," ++ c.f ++ sql")"
|
||||
|
||||
def commas(fs: Seq[Fragment]): Fragment =
|
||||
fs.reduce(_ ++ Fragment.const(",") ++ _)
|
||||
|
||||
def commas(fa: Fragment, fas: Fragment*): Fragment =
|
||||
commas(fa :: fas.toList)
|
||||
|
||||
def and(fs: Seq[Fragment]): Fragment =
|
||||
Fragment.const(" (") ++ fs
|
||||
.filter(f => !isEmpty(f))
|
||||
.reduce(_ ++ Fragment.const(" AND ") ++ _) ++ Fragment.const(") ")
|
||||
|
||||
def and(f0: Fragment, fs: Fragment*): Fragment =
|
||||
and(f0 :: fs.toList)
|
||||
|
||||
def or(fs: Seq[Fragment]): Fragment =
|
||||
Fragment.const(" (") ++ fs.reduce(_ ++ Fragment.const(" OR ") ++ _) ++ Fragment.const(
|
||||
") "
|
||||
)
|
||||
def or(f0: Fragment, fs: Fragment*): Fragment =
|
||||
or(f0 :: fs.toList)
|
||||
|
||||
def where(fa: Fragment): Fragment =
|
||||
if (isEmpty(fa)) Fragment.empty
|
||||
else Fragment.const(" WHERE ") ++ fa
|
||||
|
||||
def orderBy(fa: Fragment): Fragment =
|
||||
Fragment.const(" ORDER BY ") ++ fa
|
||||
|
||||
def orderBy(c0: Fragment, cs: Fragment*): Fragment =
|
||||
fr"ORDER BY" ++ commas(c0 :: cs.toList)
|
||||
|
||||
def updateRow(table: Fragment, where: Fragment, setter: Fragment): Fragment =
|
||||
Fragment.const("UPDATE ") ++ table ++ Fragment.const(" SET ") ++ setter ++ this.where(
|
||||
where
|
||||
)
|
||||
|
||||
def insertRow(table: Fragment, cols: List[Column], vals: Fragment): Fragment =
|
||||
Fragment.const("INSERT INTO ") ++ table ++ Fragment.const(" (") ++
|
||||
commas(cols.map(_.f)) ++ Fragment.const(") VALUES (") ++ vals ++ Fragment.const(")")
|
||||
|
||||
def insertRows(table: Fragment, cols: List[Column], vals: List[Fragment]): Fragment =
|
||||
Fragment.const("INSERT INTO ") ++ table ++ Fragment.const(" (") ++
|
||||
commas(cols.map(_.f)) ++ Fragment.const(") VALUES ") ++ commas(
|
||||
vals.map(f => sql"(" ++ f ++ sql")")
|
||||
)
|
||||
|
||||
def selectSimple(cols: Seq[Column], table: Fragment, where: Fragment): Fragment =
|
||||
selectSimple(commas(cols.map(_.f)), table, where)
|
||||
|
||||
def selectSimple(cols: Fragment, table: Fragment, where: Fragment): Fragment =
|
||||
Fragment.const("SELECT ") ++ cols ++
|
||||
Fragment.const(" FROM ") ++ table ++ this.where(where)
|
||||
|
||||
def selectDistinct(cols: Seq[Column], table: Fragment, where: Fragment): Fragment =
|
||||
Fragment.const("SELECT DISTINCT ") ++ commas(cols.map(_.f)) ++
|
||||
Fragment.const(" FROM ") ++ table ++ this.where(where)
|
||||
|
||||
def selectCount(col: Column, table: Fragment, where: Fragment): Fragment =
|
||||
Fragment.const("SELECT COUNT(") ++ col.f ++ Fragment.const(") FROM ") ++ table ++ this
|
||||
.where(
|
||||
where
|
||||
)
|
||||
|
||||
def deleteFrom(table: Fragment, where: Fragment): Fragment =
|
||||
fr"DELETE FROM" ++ table ++ this.where(where)
|
||||
|
||||
def withCTE(ps: (String, Fragment)*): Fragment = {
|
||||
val subsel: Seq[Fragment] =
|
||||
ps.map(p => Fragment.const(p._1) ++ fr"AS (" ++ p._2 ++ fr")")
|
||||
fr"WITH" ++ commas(subsel)
|
||||
}
|
||||
|
||||
def isEmpty(fragment: Fragment): Boolean =
|
||||
Fragment.empty.toString() == fragment.toString()
|
||||
|
||||
def currentTime: ConnectionIO[Timestamp] =
|
||||
Timestamp.current[ConnectionIO]
|
||||
}
|
||||
|
||||
object DoobieSyntax extends DoobieSyntax
|
@ -1,3 +0,0 @@
|
||||
package docspell.store.impl
|
||||
|
||||
object Implicits extends DoobieMeta with DoobieSyntax
|
22
modules/store/src/main/scala/docspell/store/qb/Batch.scala
Normal file
22
modules/store/src/main/scala/docspell/store/qb/Batch.scala
Normal file
@ -0,0 +1,22 @@
|
||||
package docspell.store.qb
|
||||
|
||||
case class Batch(offset: Int, limit: Int) {
|
||||
def restrictLimitTo(n: Int): Batch =
|
||||
Batch(offset, math.min(n, limit))
|
||||
|
||||
def next: Batch =
|
||||
Batch(offset + limit, limit)
|
||||
|
||||
def first: Batch =
|
||||
Batch(0, limit)
|
||||
}
|
||||
|
||||
object Batch {
|
||||
val all: Batch = Batch(0, Int.MaxValue)
|
||||
|
||||
def page(n: Int, size: Int): Batch =
|
||||
Batch(n * size, size)
|
||||
|
||||
def limit(c: Int): Batch =
|
||||
Batch(0, c)
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
package docspell.store.qb
|
||||
|
||||
case class Column[A](name: String, table: TableDef) {
|
||||
def inTable(t: TableDef): Column[A] =
|
||||
copy(table = t)
|
||||
}
|
||||
|
||||
object Column {}
|
@ -0,0 +1,86 @@
|
||||
package docspell.store.qb
|
||||
|
||||
import cats.data.NonEmptyList
|
||||
|
||||
import doobie._
|
||||
|
||||
sealed trait Condition
|
||||
|
||||
object Condition {
|
||||
case object UnitCondition extends Condition
|
||||
|
||||
val unit: Condition = UnitCondition
|
||||
|
||||
case class CompareVal[A](column: Column[A], op: Operator, value: A)(implicit
|
||||
val P: Put[A]
|
||||
) extends Condition
|
||||
|
||||
case class CompareFVal[A](dbf: DBFunction, op: Operator, value: A)(implicit
|
||||
val P: Put[A]
|
||||
) extends Condition
|
||||
|
||||
case class CompareCol[A](col1: Column[A], op: Operator, col2: Column[A])
|
||||
extends Condition
|
||||
|
||||
case class InSubSelect[A](col: Column[A], subSelect: Select) extends Condition
|
||||
case class InValues[A](col: Column[A], values: NonEmptyList[A], lower: Boolean)(implicit
|
||||
val P: Put[A]
|
||||
) extends Condition
|
||||
|
||||
case class IsNull(col: Column[_]) extends Condition
|
||||
|
||||
case class And(inner: NonEmptyList[Condition]) extends Condition {
|
||||
def append(other: Condition): And =
|
||||
other match {
|
||||
case And(otherInner) =>
|
||||
And(inner.concatNel(otherInner))
|
||||
case _ =>
|
||||
And(inner.append(other))
|
||||
}
|
||||
}
|
||||
object And {
|
||||
def apply(c: Condition, cs: Condition*): And =
|
||||
And(NonEmptyList(c, cs.toList))
|
||||
|
||||
object Inner extends InnerCondition {
|
||||
def unapply(node: Condition): Option[NonEmptyList[Condition]] =
|
||||
node match {
|
||||
case n: And =>
|
||||
Option(n.inner)
|
||||
case _ =>
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
case class Or(inner: NonEmptyList[Condition]) extends Condition {
|
||||
def append(other: Condition): Or =
|
||||
other match {
|
||||
case Or(otherInner) =>
|
||||
Or(inner.concatNel(otherInner))
|
||||
case _ =>
|
||||
Or(inner.append(other))
|
||||
}
|
||||
}
|
||||
object Or {
|
||||
def apply(c: Condition, cs: Condition*): Or =
|
||||
Or(NonEmptyList(c, cs.toList))
|
||||
|
||||
object Inner extends InnerCondition {
|
||||
def unapply(node: Condition): Option[NonEmptyList[Condition]] =
|
||||
node match {
|
||||
case n: Or =>
|
||||
Option(n.inner)
|
||||
case _ =>
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
case class Not(c: Condition) extends Condition
|
||||
object Not {}
|
||||
|
||||
trait InnerCondition {
|
||||
def unapply(node: Condition): Option[NonEmptyList[Condition]]
|
||||
}
|
||||
}
|
12
modules/store/src/main/scala/docspell/store/qb/CteBind.scala
Normal file
12
modules/store/src/main/scala/docspell/store/qb/CteBind.scala
Normal file
@ -0,0 +1,12 @@
|
||||
package docspell.store.qb
|
||||
|
||||
case class CteBind(name: TableDef, coldef: Vector[Column[_]], select: Select) {}
|
||||
|
||||
object CteBind {
|
||||
|
||||
def apply(t: (TableDef, Select)): CteBind =
|
||||
CteBind(t._1, Vector.empty, t._2)
|
||||
|
||||
def apply(name: TableDef, col: Column[_], cols: Column[_]*)(select: Select): CteBind =
|
||||
CteBind(name, cols.toVector.prepended(col), select)
|
||||
}
|
@ -0,0 +1,40 @@
|
||||
package docspell.store.qb
|
||||
|
||||
sealed trait DBFunction {}
|
||||
|
||||
object DBFunction {
|
||||
|
||||
val countAll: DBFunction = CountAll
|
||||
|
||||
def countAs[A](column: Column[A]): DBFunction =
|
||||
Count(column)
|
||||
|
||||
case object CountAll extends DBFunction
|
||||
|
||||
case class Count(column: Column[_]) extends DBFunction
|
||||
|
||||
case class Max(expr: SelectExpr) extends DBFunction
|
||||
|
||||
case class Min(expr: SelectExpr) extends DBFunction
|
||||
|
||||
case class Coalesce(expr: SelectExpr, exprs: Vector[SelectExpr]) extends DBFunction
|
||||
|
||||
case class Power(expr: SelectExpr, base: Int) extends DBFunction
|
||||
|
||||
case class Calc(op: Operator, left: SelectExpr, right: SelectExpr) extends DBFunction
|
||||
|
||||
case class Substring(expr: SelectExpr, start: Int, length: Int) extends DBFunction
|
||||
|
||||
case class Cast(expr: SelectExpr, newType: String) extends DBFunction
|
||||
|
||||
case class Avg(expr: SelectExpr) extends DBFunction
|
||||
|
||||
case class Sum(expr: SelectExpr) extends DBFunction
|
||||
|
||||
sealed trait Operator
|
||||
object Operator {
|
||||
case object Plus extends Operator
|
||||
case object Minus extends Operator
|
||||
case object Mult extends Operator
|
||||
}
|
||||
}
|
85
modules/store/src/main/scala/docspell/store/qb/DML.scala
Normal file
85
modules/store/src/main/scala/docspell/store/qb/DML.scala
Normal file
@ -0,0 +1,85 @@
|
||||
package docspell.store.qb
|
||||
|
||||
import cats.data.{NonEmptyList => Nel}
|
||||
|
||||
import docspell.store.qb.impl._
|
||||
|
||||
import doobie._
|
||||
import doobie.implicits._
|
||||
|
||||
object DML {
|
||||
private val comma = fr","
|
||||
|
||||
def delete(table: TableDef, cond: Condition): ConnectionIO[Int] =
|
||||
deleteFragment(table, cond).update.run
|
||||
|
||||
def deleteFragment(table: TableDef, cond: Condition): Fragment =
|
||||
fr"DELETE FROM" ++ FromExprBuilder.buildTable(table) ++ fr"WHERE" ++ ConditionBuilder
|
||||
.build(cond)
|
||||
|
||||
def insert(table: TableDef, cols: Nel[Column[_]], values: Fragment): ConnectionIO[Int] =
|
||||
insertFragment(table, cols, List(values)).update.run
|
||||
|
||||
def insertMany(
|
||||
table: TableDef,
|
||||
cols: Nel[Column[_]],
|
||||
values: Seq[Fragment]
|
||||
): ConnectionIO[Int] =
|
||||
insertFragment(table, cols, values).update.run
|
||||
|
||||
def insertFragment(
|
||||
table: TableDef,
|
||||
cols: Nel[Column[_]],
|
||||
values: Seq[Fragment]
|
||||
): Fragment =
|
||||
fr"INSERT INTO" ++ FromExprBuilder.buildTable(table) ++ sql"(" ++
|
||||
cols
|
||||
.map(SelectExprBuilder.columnNoPrefix)
|
||||
.reduceLeft(_ ++ comma ++ _) ++ fr") VALUES" ++
|
||||
values.map(f => sql"(" ++ f ++ sql")").reduce(_ ++ comma ++ _)
|
||||
|
||||
def update(
|
||||
table: TableDef,
|
||||
cond: Condition,
|
||||
setter: Nel[Setter[_]]
|
||||
): ConnectionIO[Int] =
|
||||
updateFragment(table, Some(cond), setter).update.run
|
||||
|
||||
def updateFragment(
|
||||
table: TableDef,
|
||||
cond: Option[Condition],
|
||||
setter: Nel[Setter[_]]
|
||||
): Fragment = {
|
||||
val condFrag = cond.map(SelectBuilder.cond).getOrElse(Fragment.empty)
|
||||
fr"UPDATE" ++ FromExprBuilder.buildTable(table) ++ fr"SET" ++
|
||||
setter
|
||||
.map(s => buildSetter(s))
|
||||
.reduceLeft(_ ++ comma ++ _) ++
|
||||
condFrag
|
||||
}
|
||||
|
||||
private def buildSetter[A](setter: Setter[A]): Fragment =
|
||||
setter match {
|
||||
case s @ Setter.SetValue(column, value) =>
|
||||
SelectExprBuilder.columnNoPrefix(column) ++ fr" =" ++ ConditionBuilder.buildValue(
|
||||
value
|
||||
)(s.P)
|
||||
|
||||
case s @ Setter.SetOptValue(column, optValue) =>
|
||||
SelectExprBuilder.columnNoPrefix(column) ++ fr" =" ++ ConditionBuilder
|
||||
.buildOptValue(
|
||||
optValue
|
||||
)(s.P)
|
||||
|
||||
case Setter.Increment(column, amount) =>
|
||||
val colFrag = SelectExprBuilder.columnNoPrefix(column)
|
||||
colFrag ++ fr" =" ++ colFrag ++ fr" + $amount"
|
||||
|
||||
case Setter.Decrement(column, amount) =>
|
||||
val colFrag = SelectExprBuilder.columnNoPrefix(column)
|
||||
colFrag ++ fr" =" ++ colFrag ++ fr" - $amount"
|
||||
}
|
||||
|
||||
def set(s: Setter[_], more: Setter[_]*): Nel[Setter[_]] =
|
||||
Nel(s, more.toList)
|
||||
}
|
304
modules/store/src/main/scala/docspell/store/qb/DSL.scala
Normal file
304
modules/store/src/main/scala/docspell/store/qb/DSL.scala
Normal file
@ -0,0 +1,304 @@
|
||||
package docspell.store.qb
|
||||
|
||||
import cats.data.{NonEmptyList => Nel}
|
||||
|
||||
import docspell.store.impl.DoobieMeta
|
||||
import docspell.store.qb.impl.SelectBuilder
|
||||
|
||||
import doobie.{Fragment, Put}
|
||||
|
||||
trait DSL extends DoobieMeta {
|
||||
|
||||
def run(projection: Nel[SelectExpr], from: FromExpr): Fragment =
|
||||
SelectBuilder(Select(projection, from))
|
||||
|
||||
def run(projection: Nel[SelectExpr], from: FromExpr, where: Condition): Fragment =
|
||||
SelectBuilder(Select(projection, from, where))
|
||||
|
||||
def runDistinct(
|
||||
projection: Nel[SelectExpr],
|
||||
from: FromExpr,
|
||||
where: Condition
|
||||
): Fragment =
|
||||
SelectBuilder(Select(projection, from, where).distinct)
|
||||
|
||||
def withCte(cte: (TableDef, Select), more: (TableDef, Select)*): DSL.WithCteDsl =
|
||||
DSL.WithCteDsl(CteBind(cte), more.map(CteBind.apply).toVector)
|
||||
|
||||
def withCte(
|
||||
name: TableDef,
|
||||
col: Column[_],
|
||||
cols: Column[_]*
|
||||
): Select => DSL.WithCteDsl =
|
||||
sel => DSL.WithCteDsl(CteBind(name, col, cols: _*)(sel), Vector.empty)
|
||||
|
||||
def select(cond: Condition): Nel[SelectExpr] =
|
||||
Nel.of(SelectExpr.SelectCondition(cond, None))
|
||||
|
||||
def select(dbf: DBFunction): Nel[SelectExpr] =
|
||||
Nel.of(SelectExpr.SelectFun(dbf, None))
|
||||
|
||||
def select(e: SelectExpr, es: SelectExpr*): Nel[SelectExpr] =
|
||||
Nel(e, es.toList)
|
||||
|
||||
def select(c: Column[_], cs: Column[_]*): Nel[SelectExpr] =
|
||||
Nel(c, cs.toList).map(col => SelectExpr.SelectColumn(col, None))
|
||||
|
||||
def select(seq: Nel[Column[_]], seqs: Nel[Column[_]]*): Nel[SelectExpr] =
|
||||
seqs.foldLeft(seq)(_ concatNel _).map(c => SelectExpr.SelectColumn(c, None))
|
||||
|
||||
def union(s1: Select, sn: Select*): Select =
|
||||
Select.Union(s1, sn.toVector)
|
||||
|
||||
def intersect(s1: Select, sn: Select*): Select =
|
||||
Select.Intersect(s1, sn.toVector)
|
||||
|
||||
def intersect(nel: Nel[Select]): Select =
|
||||
Select.Intersect(nel.head, nel.tail.toVector)
|
||||
|
||||
def from(table: TableDef): FromExpr.From =
|
||||
FromExpr.From(table)
|
||||
|
||||
def from(sel: Select, alias: String): FromExpr.From =
|
||||
FromExpr.From(sel, alias)
|
||||
|
||||
def count(c: Column[_]): DBFunction =
|
||||
DBFunction.Count(c)
|
||||
|
||||
def countAll: DBFunction =
|
||||
DBFunction.CountAll
|
||||
|
||||
def max(e: SelectExpr): DBFunction =
|
||||
DBFunction.Max(e)
|
||||
|
||||
def max(c: Column[_]): DBFunction =
|
||||
max(c.s)
|
||||
|
||||
def min(expr: SelectExpr): DBFunction =
|
||||
DBFunction.Min(expr)
|
||||
|
||||
def min(c: Column[_]): DBFunction =
|
||||
min(c.s)
|
||||
|
||||
def avg(expr: SelectExpr): DBFunction =
|
||||
DBFunction.Avg(expr)
|
||||
|
||||
def sum(expr: SelectExpr): DBFunction =
|
||||
DBFunction.Sum(expr)
|
||||
|
||||
def cast(expr: SelectExpr, targetType: String): DBFunction =
|
||||
DBFunction.Cast(expr, targetType)
|
||||
|
||||
def coalesce(expr: SelectExpr, more: SelectExpr*): DBFunction.Coalesce =
|
||||
DBFunction.Coalesce(expr, more.toVector)
|
||||
|
||||
def power(base: Int, expr: SelectExpr): DBFunction =
|
||||
DBFunction.Power(expr, base)
|
||||
|
||||
def substring(expr: SelectExpr, start: Int, length: Int): DBFunction =
|
||||
DBFunction.Substring(expr, start, length)
|
||||
|
||||
def lit[A](value: A)(implicit P: Put[A]): SelectExpr.SelectLit[A] =
|
||||
SelectExpr.SelectLit(value, None)
|
||||
|
||||
def plus(left: SelectExpr, right: SelectExpr): DBFunction =
|
||||
DBFunction.Calc(DBFunction.Operator.Plus, left, right)
|
||||
|
||||
def mult(left: SelectExpr, right: SelectExpr): DBFunction =
|
||||
DBFunction.Calc(DBFunction.Operator.Mult, left, right)
|
||||
|
||||
def and(c: Condition, cs: Condition*): Condition =
|
||||
c match {
|
||||
case a: Condition.And =>
|
||||
cs.foldLeft(a)(_.append(_))
|
||||
case _ =>
|
||||
Condition.And(c, cs: _*)
|
||||
}
|
||||
|
||||
def or(c: Condition, cs: Condition*): Condition =
|
||||
c match {
|
||||
case o: Condition.Or =>
|
||||
cs.foldLeft(o)(_.append(_))
|
||||
case _ =>
|
||||
Condition.Or(c, cs: _*)
|
||||
}
|
||||
|
||||
def not(c: Condition): Condition =
|
||||
c match {
|
||||
case Condition.Not(el) =>
|
||||
el
|
||||
case _ =>
|
||||
Condition.Not(c)
|
||||
}
|
||||
|
||||
def where(c: Condition, cs: Condition*): Condition =
|
||||
if (cs.isEmpty) c
|
||||
else and(c, cs: _*)
|
||||
|
||||
implicit final class ColumnOps[A](col: Column[A]) {
|
||||
def s: SelectExpr =
|
||||
SelectExpr.SelectColumn(col, None)
|
||||
def as(alias: String): SelectExpr =
|
||||
SelectExpr.SelectColumn(col, Some(alias))
|
||||
def as(otherCol: Column[A]): SelectExpr =
|
||||
SelectExpr.SelectColumn(col, Some(otherCol.name))
|
||||
|
||||
def setTo(value: A)(implicit P: Put[A]): Setter[A] =
|
||||
Setter.SetValue(col, value)
|
||||
|
||||
def setTo(value: Option[A])(implicit P: Put[A]): Setter[Option[A]] =
|
||||
Setter.SetOptValue(col, value)
|
||||
|
||||
def increment(amount: Int): Setter[A] =
|
||||
Setter.Increment(col, amount)
|
||||
|
||||
def decrement(amount: Int): Setter[A] =
|
||||
Setter.Decrement(col, amount)
|
||||
|
||||
def asc: OrderBy =
|
||||
OrderBy(SelectExpr.SelectColumn(col, None), OrderBy.OrderType.Asc)
|
||||
|
||||
def desc: OrderBy =
|
||||
OrderBy(SelectExpr.SelectColumn(col, None), OrderBy.OrderType.Desc)
|
||||
|
||||
def ===(value: A)(implicit P: Put[A]): Condition =
|
||||
Condition.CompareVal(col, Operator.Eq, value)
|
||||
|
||||
def ====(value: String): Condition =
|
||||
Condition.CompareVal(col.asInstanceOf[Column[String]], Operator.Eq, value)
|
||||
|
||||
def like(value: A)(implicit P: Put[A]): Condition =
|
||||
Condition.CompareVal(col, Operator.LowerLike, value)
|
||||
|
||||
def likes(value: String): Condition =
|
||||
Condition.CompareVal(col.asInstanceOf[Column[String]], Operator.LowerLike, value)
|
||||
|
||||
def <=(value: A)(implicit P: Put[A]): Condition =
|
||||
Condition.CompareVal(col, Operator.Lte, value)
|
||||
|
||||
def >=(value: A)(implicit P: Put[A]): Condition =
|
||||
Condition.CompareVal(col, Operator.Gte, value)
|
||||
|
||||
def >(value: A)(implicit P: Put[A]): Condition =
|
||||
Condition.CompareVal(col, Operator.Gt, value)
|
||||
|
||||
def <(value: A)(implicit P: Put[A]): Condition =
|
||||
Condition.CompareVal(col, Operator.Lt, value)
|
||||
|
||||
def <>(value: A)(implicit P: Put[A]): Condition =
|
||||
Condition.CompareVal(col, Operator.Neq, value)
|
||||
|
||||
def in(subsel: Select): Condition =
|
||||
Condition.InSubSelect(col, subsel)
|
||||
|
||||
def notIn(subsel: Select): Condition =
|
||||
in(subsel).negate
|
||||
|
||||
def in(values: Nel[A])(implicit P: Put[A]): Condition =
|
||||
Condition.InValues(col, values, false)
|
||||
|
||||
def notIn(values: Nel[A])(implicit P: Put[A]): Condition =
|
||||
in(values).negate
|
||||
|
||||
def inLower(values: Nel[A])(implicit P: Put[A]): Condition =
|
||||
Condition.InValues(col, values, true)
|
||||
|
||||
def notInLower(values: Nel[A])(implicit P: Put[A]): Condition =
|
||||
Condition.InValues(col, values, true).negate
|
||||
|
||||
def isNull: Condition =
|
||||
Condition.IsNull(col)
|
||||
|
||||
def isNotNull: Condition =
|
||||
Condition.IsNull(col).negate
|
||||
|
||||
def ===(other: Column[A]): Condition =
|
||||
Condition.CompareCol(col, Operator.Eq, other)
|
||||
|
||||
def <>(other: Column[A]): Condition =
|
||||
Condition.CompareCol(col, Operator.Neq, other)
|
||||
}
|
||||
|
||||
implicit final class ConditionOps(c: Condition) {
|
||||
def s: SelectExpr =
|
||||
SelectExpr.SelectCondition(c, None)
|
||||
|
||||
def as(alias: String): SelectExpr =
|
||||
SelectExpr.SelectCondition(c, Some(alias))
|
||||
|
||||
def &&(other: Condition): Condition =
|
||||
and(c, other)
|
||||
|
||||
def &&?(other: Option[Condition]): Condition =
|
||||
other.map(ce => &&(ce)).getOrElse(c)
|
||||
|
||||
def ||(other: Condition): Condition =
|
||||
or(c, other)
|
||||
|
||||
def ||?(other: Option[Condition]): Condition =
|
||||
other.map(ce => ||(ce)).getOrElse(c)
|
||||
|
||||
def negate: Condition =
|
||||
not(c)
|
||||
|
||||
def unary_! : Condition =
|
||||
not(c)
|
||||
}
|
||||
|
||||
implicit final class DBFunctionOps(dbf: DBFunction) {
|
||||
def s: SelectExpr =
|
||||
SelectExpr.SelectFun(dbf, None)
|
||||
def as(alias: String): SelectExpr =
|
||||
SelectExpr.SelectFun(dbf, Some(alias))
|
||||
|
||||
def as(otherCol: Column[_]): SelectExpr =
|
||||
SelectExpr.SelectFun(dbf, Some(otherCol.name))
|
||||
|
||||
def ===[A](value: A)(implicit P: Put[A]): Condition =
|
||||
Condition.CompareFVal(dbf, Operator.Eq, value)
|
||||
|
||||
def ====(value: String): Condition =
|
||||
Condition.CompareFVal(dbf, Operator.Eq, value)
|
||||
|
||||
def like[A](value: A)(implicit P: Put[A]): Condition =
|
||||
Condition.CompareFVal(dbf, Operator.LowerLike, value)
|
||||
|
||||
def likes(value: String): Condition =
|
||||
Condition.CompareFVal(dbf, Operator.LowerLike, value)
|
||||
|
||||
def <=[A](value: A)(implicit P: Put[A]): Condition =
|
||||
Condition.CompareFVal(dbf, Operator.Lte, value)
|
||||
|
||||
def >=[A](value: A)(implicit P: Put[A]): Condition =
|
||||
Condition.CompareFVal(dbf, Operator.Gte, value)
|
||||
|
||||
def >[A](value: A)(implicit P: Put[A]): Condition =
|
||||
Condition.CompareFVal(dbf, Operator.Gt, value)
|
||||
|
||||
def <[A](value: A)(implicit P: Put[A]): Condition =
|
||||
Condition.CompareFVal(dbf, Operator.Lt, value)
|
||||
|
||||
def <>[A](value: A)(implicit P: Put[A]): Condition =
|
||||
Condition.CompareFVal(dbf, Operator.Neq, value)
|
||||
|
||||
def -[A](value: A)(implicit P: Put[A]): DBFunction =
|
||||
DBFunction.Calc(
|
||||
DBFunction.Operator.Minus,
|
||||
SelectExpr.SelectFun(dbf, None),
|
||||
SelectExpr.SelectLit(value, None)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
object DSL extends DSL {
|
||||
|
||||
final case class WithCteDsl(cte: CteBind, ctes: Vector[CteBind]) {
|
||||
|
||||
def select(s: Select): Select.WithCte =
|
||||
Select.WithCte(cte, ctes, s)
|
||||
|
||||
def apply(s: Select): Select.WithCte =
|
||||
select(s)
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,82 @@
|
||||
package docspell.store.qb
|
||||
|
||||
import docspell.store.qb.FromExpr.{Joined, Relation}
|
||||
|
||||
sealed trait FromExpr {
|
||||
def innerJoin(other: Relation, on: Condition): Joined
|
||||
|
||||
def innerJoin(other: TableDef, on: Condition): Joined =
|
||||
innerJoin(Relation.Table(other), on)
|
||||
|
||||
def leftJoin(other: Relation, on: Condition): Joined
|
||||
|
||||
def leftJoin(other: TableDef, on: Condition): Joined =
|
||||
leftJoin(Relation.Table(other), on)
|
||||
|
||||
def leftJoin(sel: Select, alias: String, on: Condition): Joined =
|
||||
leftJoin(Relation.SubSelect(sel, alias), on)
|
||||
|
||||
/** Prepends the given from expression to existing joins. It will
|
||||
* replace the current [[FromExpr.From]] value.
|
||||
*
|
||||
* If this is a [[FromExpr.From]], it is replaced by the given
|
||||
* expression. If this is a [[FromExpr.Joined]] then the given
|
||||
* expression replaces the current `From` and the joins are
|
||||
* prepended to the existing joins.
|
||||
*/
|
||||
def prepend(fe: FromExpr): FromExpr
|
||||
}
|
||||
|
||||
object FromExpr {
|
||||
|
||||
case class From(table: Relation) extends FromExpr {
|
||||
def innerJoin(other: Relation, on: Condition): Joined =
|
||||
Joined(this, Vector(Join.InnerJoin(other, on)))
|
||||
|
||||
def leftJoin(other: Relation, on: Condition): Joined =
|
||||
Joined(this, Vector(Join.LeftJoin(other, on)))
|
||||
|
||||
def prepend(fe: FromExpr): FromExpr =
|
||||
fe
|
||||
}
|
||||
|
||||
object From {
|
||||
def apply(td: TableDef): From =
|
||||
From(Relation.Table(td))
|
||||
|
||||
def apply(select: Select, alias: String): From =
|
||||
From(Relation.SubSelect(select, alias))
|
||||
}
|
||||
|
||||
case class Joined(from: From, joins: Vector[Join]) extends FromExpr {
|
||||
def innerJoin(other: Relation, on: Condition): Joined =
|
||||
Joined(from, joins :+ Join.InnerJoin(other, on))
|
||||
|
||||
def leftJoin(other: Relation, on: Condition): Joined =
|
||||
Joined(from, joins :+ Join.LeftJoin(other, on))
|
||||
|
||||
def prepend(fe: FromExpr): FromExpr =
|
||||
fe match {
|
||||
case f: From =>
|
||||
Joined(f, joins)
|
||||
case Joined(f, js) =>
|
||||
Joined(f, js ++ joins)
|
||||
}
|
||||
}
|
||||
|
||||
sealed trait Relation
|
||||
object Relation {
|
||||
final case class Table(table: TableDef) extends Relation
|
||||
final case class SubSelect(select: Select, alias: String) extends Relation {
|
||||
def as(a: String): SubSelect =
|
||||
copy(alias = a)
|
||||
}
|
||||
}
|
||||
|
||||
sealed trait Join
|
||||
object Join {
|
||||
final case class InnerJoin(table: Relation, cond: Condition) extends Join
|
||||
final case class LeftJoin(table: Relation, cond: Condition) extends Join
|
||||
}
|
||||
|
||||
}
|
18
modules/store/src/main/scala/docspell/store/qb/GroupBy.scala
Normal file
18
modules/store/src/main/scala/docspell/store/qb/GroupBy.scala
Normal file
@ -0,0 +1,18 @@
|
||||
package docspell.store.qb
|
||||
|
||||
import cats.data.NonEmptyList
|
||||
|
||||
case class GroupBy(name: SelectExpr, names: Vector[SelectExpr], having: Option[Condition])
|
||||
|
||||
object GroupBy {
|
||||
|
||||
def apply(c: Column[_], cs: Column[_]*): GroupBy =
|
||||
GroupBy(
|
||||
SelectExpr.SelectColumn(c, None),
|
||||
cs.toVector.map(c => SelectExpr.SelectColumn(c, None)),
|
||||
None
|
||||
)
|
||||
|
||||
def apply(nel: NonEmptyList[Column[_]]): GroupBy =
|
||||
apply(nel.head, nel.tail: _*)
|
||||
}
|
@ -0,0 +1,15 @@
|
||||
package docspell.store.qb
|
||||
|
||||
sealed trait Operator
|
||||
|
||||
object Operator {
|
||||
|
||||
case object Eq extends Operator
|
||||
case object Neq extends Operator
|
||||
case object Gt extends Operator
|
||||
case object Lt extends Operator
|
||||
case object Gte extends Operator
|
||||
case object Lte extends Operator
|
||||
case object LowerLike extends Operator
|
||||
|
||||
}
|
20
modules/store/src/main/scala/docspell/store/qb/OrderBy.scala
Normal file
20
modules/store/src/main/scala/docspell/store/qb/OrderBy.scala
Normal file
@ -0,0 +1,20 @@
|
||||
package docspell.store.qb
|
||||
|
||||
import docspell.store.qb.OrderBy.OrderType
|
||||
|
||||
final case class OrderBy(expr: SelectExpr, orderType: OrderType)
|
||||
|
||||
object OrderBy {
|
||||
|
||||
def asc(e: SelectExpr): OrderBy =
|
||||
OrderBy(e, OrderType.Asc)
|
||||
|
||||
def desc(e: SelectExpr): OrderBy =
|
||||
OrderBy(e, OrderType.Desc)
|
||||
|
||||
sealed trait OrderType
|
||||
object OrderType {
|
||||
case object Asc extends OrderType
|
||||
case object Desc extends OrderType
|
||||
}
|
||||
}
|
286
modules/store/src/main/scala/docspell/store/qb/Select.scala
Normal file
286
modules/store/src/main/scala/docspell/store/qb/Select.scala
Normal file
@ -0,0 +1,286 @@
|
||||
package docspell.store.qb
|
||||
|
||||
import cats.data.{NonEmptyList => Nel}
|
||||
|
||||
import docspell.store.qb.impl.SelectBuilder
|
||||
|
||||
import doobie._
|
||||
|
||||
/** A sql select statement that allows to change certain parts of the query.
|
||||
*/
|
||||
sealed trait Select {
|
||||
|
||||
/** Builds the sql select statement into a doobie fragment.
|
||||
*/
|
||||
def build: Fragment =
|
||||
SelectBuilder(this)
|
||||
|
||||
/** When using this as a sub-select, an alias is required.
|
||||
*/
|
||||
def as(alias: String): SelectExpr.SelectQuery =
|
||||
SelectExpr.SelectQuery(this, Some(alias))
|
||||
|
||||
/** Adds one or more order-by definitions */
|
||||
def orderBy(ob: OrderBy, obs: OrderBy*): Select
|
||||
|
||||
/** Uses the given column for ordering asc */
|
||||
def orderBy(c: Column[_]): Select =
|
||||
orderBy(OrderBy(SelectExpr.SelectColumn(c, None), OrderBy.OrderType.Asc))
|
||||
|
||||
def groupBy(gb: GroupBy): Select
|
||||
|
||||
def groupBy(c: Column[_], cs: Column[_]*): Select =
|
||||
groupBy(GroupBy(c, cs: _*))
|
||||
|
||||
def limit(batch: Batch): Select =
|
||||
this match {
|
||||
case Select.Limit(q, _) =>
|
||||
Select.Limit(q, batch)
|
||||
case _ =>
|
||||
Select.Limit(this, batch)
|
||||
}
|
||||
|
||||
def limit(n: Int): Select =
|
||||
limit(Batch.limit(n))
|
||||
|
||||
def appendCte(next: CteBind): Select =
|
||||
this match {
|
||||
case Select.WithCte(cte, ctes, query) =>
|
||||
Select.WithCte(cte, ctes :+ next, query)
|
||||
case _ =>
|
||||
Select.WithCte(next, Vector.empty, this)
|
||||
}
|
||||
|
||||
def appendSelect(e: SelectExpr): Select
|
||||
|
||||
def withSelect(e: Nel[SelectExpr]): Select
|
||||
|
||||
def changeFrom(f: FromExpr => FromExpr): Select
|
||||
|
||||
def changeWhere(f: Condition => Condition): Select
|
||||
|
||||
def where(c: Option[Condition]): Select =
|
||||
where(c.getOrElse(Condition.unit))
|
||||
|
||||
def where(c: Condition): Select
|
||||
|
||||
def unwrap: Select.SimpleSelect
|
||||
}
|
||||
|
||||
object Select {
|
||||
def apply(projection: Nel[SelectExpr], from: FromExpr) =
|
||||
SimpleSelect(false, projection, from, Condition.unit, None)
|
||||
|
||||
def apply(projection: SelectExpr, from: FromExpr) =
|
||||
SimpleSelect(false, Nel.of(projection), from, Condition.unit, None)
|
||||
|
||||
def apply(
|
||||
projection: Nel[SelectExpr],
|
||||
from: FromExpr,
|
||||
where: Condition
|
||||
) = SimpleSelect(false, projection, from, where, None)
|
||||
|
||||
def apply(
|
||||
projection: SelectExpr,
|
||||
from: FromExpr,
|
||||
where: Condition
|
||||
) = SimpleSelect(false, Nel.of(projection), from, where, None)
|
||||
|
||||
def apply(
|
||||
projection: Nel[SelectExpr],
|
||||
from: FromExpr,
|
||||
where: Condition,
|
||||
groupBy: GroupBy
|
||||
) = SimpleSelect(false, projection, from, where, Some(groupBy))
|
||||
|
||||
case class SimpleSelect(
|
||||
distinctFlag: Boolean,
|
||||
projection: Nel[SelectExpr],
|
||||
from: FromExpr,
|
||||
where: Condition,
|
||||
groupBy: Option[GroupBy]
|
||||
) extends Select {
|
||||
def unwrap: Select.SimpleSelect =
|
||||
this
|
||||
|
||||
def groupBy(gb: GroupBy): SimpleSelect =
|
||||
copy(groupBy = Some(gb))
|
||||
|
||||
def distinct: SimpleSelect =
|
||||
copy(distinctFlag = true)
|
||||
|
||||
def noDistinct: SimpleSelect =
|
||||
copy(distinctFlag = false)
|
||||
|
||||
def where(c: Condition): SimpleSelect =
|
||||
copy(where = c)
|
||||
|
||||
def appendSelect(e: SelectExpr): SimpleSelect =
|
||||
copy(projection = projection.append(e))
|
||||
|
||||
def withSelect(es: Nel[SelectExpr]): SimpleSelect =
|
||||
copy(projection = es)
|
||||
|
||||
def changeFrom(f: FromExpr => FromExpr): SimpleSelect =
|
||||
copy(from = f(from))
|
||||
|
||||
def changeWhere(f: Condition => Condition): SimpleSelect =
|
||||
copy(where = f(where))
|
||||
|
||||
def orderBy(ob: OrderBy, obs: OrderBy*): Ordered =
|
||||
Ordered(this, ob, obs.toVector)
|
||||
}
|
||||
|
||||
case class RawSelect(fragment: Fragment) extends Select {
|
||||
def unwrap: Select.SimpleSelect =
|
||||
sys.error("Cannot unwrap RawSelect")
|
||||
|
||||
def groupBy(gb: GroupBy): Select =
|
||||
sys.error("RawSelect doesn't support adding group by clause")
|
||||
|
||||
def appendSelect(e: SelectExpr): RawSelect =
|
||||
sys.error("RawSelect doesn't support appending to select list")
|
||||
|
||||
def changeFrom(f: FromExpr => FromExpr): Select =
|
||||
sys.error("RawSelect doesn't support changing from expression")
|
||||
|
||||
def changeWhere(f: Condition => Condition): Select =
|
||||
sys.error("RawSelect doesn't support changing where condition")
|
||||
|
||||
def orderBy(ob: OrderBy, obs: OrderBy*): Ordered =
|
||||
sys.error("RawSelect doesn't support adding orderBy clause")
|
||||
|
||||
def where(c: Condition): Select =
|
||||
sys.error("RawSelect doesn't support adding where clause")
|
||||
|
||||
def withSelect(es: Nel[SelectExpr]): Select =
|
||||
sys.error("RawSelect doesn't support changing select list")
|
||||
}
|
||||
|
||||
case class Union(q: Select, qs: Vector[Select]) extends Select {
|
||||
def unwrap: Select.SimpleSelect =
|
||||
q.unwrap
|
||||
|
||||
def groupBy(gb: GroupBy): Union =
|
||||
copy(q = q.groupBy(gb))
|
||||
|
||||
def appendSelect(e: SelectExpr): Union =
|
||||
copy(q = q.appendSelect(e))
|
||||
|
||||
def changeFrom(f: FromExpr => FromExpr): Union =
|
||||
copy(q = q.changeFrom(f))
|
||||
|
||||
def changeWhere(f: Condition => Condition): Union =
|
||||
copy(q = q.changeWhere(f))
|
||||
|
||||
def orderBy(ob: OrderBy, obs: OrderBy*): Ordered =
|
||||
Ordered(this, ob, obs.toVector)
|
||||
|
||||
def where(c: Condition): Union =
|
||||
copy(q = q.where(c))
|
||||
|
||||
def withSelect(es: Nel[SelectExpr]): Union =
|
||||
copy(q = q.withSelect(es))
|
||||
}
|
||||
|
||||
case class Intersect(q: Select, qs: Vector[Select]) extends Select {
|
||||
def unwrap: Select.SimpleSelect =
|
||||
q.unwrap
|
||||
|
||||
def groupBy(gb: GroupBy): Intersect =
|
||||
copy(q = q.groupBy(gb))
|
||||
|
||||
def appendSelect(e: SelectExpr): Intersect =
|
||||
copy(q = q.appendSelect(e))
|
||||
|
||||
def changeFrom(f: FromExpr => FromExpr): Intersect =
|
||||
copy(q = q.changeFrom(f))
|
||||
|
||||
def changeWhere(f: Condition => Condition): Intersect =
|
||||
copy(q = q.changeWhere(f))
|
||||
|
||||
def orderBy(ob: OrderBy, obs: OrderBy*): Ordered =
|
||||
Ordered(this, ob, obs.toVector)
|
||||
|
||||
def where(c: Condition): Intersect =
|
||||
copy(q = q.where(c))
|
||||
|
||||
def withSelect(es: Nel[SelectExpr]): Intersect =
|
||||
copy(q = q.withSelect(es))
|
||||
}
|
||||
|
||||
case class Ordered(q: Select, orderBy: OrderBy, orderBys: Vector[OrderBy])
|
||||
extends Select {
|
||||
def unwrap: Select.SimpleSelect =
|
||||
q.unwrap
|
||||
|
||||
def groupBy(gb: GroupBy): Ordered =
|
||||
copy(q = q.groupBy(gb))
|
||||
|
||||
def appendSelect(e: SelectExpr): Ordered =
|
||||
copy(q = q.appendSelect(e))
|
||||
def changeFrom(f: FromExpr => FromExpr): Ordered =
|
||||
copy(q = q.changeFrom(f))
|
||||
def changeWhere(f: Condition => Condition): Ordered =
|
||||
copy(q = q.changeWhere(f))
|
||||
|
||||
def orderBy(ob: OrderBy, obs: OrderBy*): Ordered =
|
||||
Ordered(q, ob, obs.toVector)
|
||||
|
||||
def where(c: Condition): Ordered =
|
||||
copy(q = q.where(c))
|
||||
|
||||
def withSelect(es: Nel[SelectExpr]): Ordered =
|
||||
copy(q = q.withSelect(es))
|
||||
}
|
||||
|
||||
case class Limit(q: Select, batch: Batch) extends Select {
|
||||
def unwrap: Select.SimpleSelect =
|
||||
q.unwrap
|
||||
|
||||
def groupBy(gb: GroupBy): Limit =
|
||||
copy(q = q.groupBy(gb))
|
||||
|
||||
def appendSelect(e: SelectExpr): Limit =
|
||||
copy(q = q.appendSelect(e))
|
||||
def changeFrom(f: FromExpr => FromExpr): Limit =
|
||||
copy(q = q.changeFrom(f))
|
||||
def changeWhere(f: Condition => Condition): Limit =
|
||||
copy(q = q.changeWhere(f))
|
||||
|
||||
def orderBy(ob: OrderBy, obs: OrderBy*): Limit =
|
||||
copy(q = q.orderBy(ob, obs: _*))
|
||||
|
||||
def where(c: Condition): Limit =
|
||||
copy(q = q.where(c))
|
||||
|
||||
def withSelect(es: Nel[SelectExpr]): Limit =
|
||||
copy(q = q.withSelect(es))
|
||||
}
|
||||
|
||||
case class WithCte(cte: CteBind, ctes: Vector[CteBind], q: Select) extends Select {
|
||||
def unwrap: Select.SimpleSelect =
|
||||
q.unwrap
|
||||
|
||||
def groupBy(gb: GroupBy): WithCte =
|
||||
copy(q = q.groupBy(gb))
|
||||
|
||||
def appendSelect(e: SelectExpr): WithCte =
|
||||
copy(q = q.appendSelect(e))
|
||||
|
||||
def changeFrom(f: FromExpr => FromExpr): WithCte =
|
||||
copy(q = q.changeFrom(f))
|
||||
|
||||
def changeWhere(f: Condition => Condition): WithCte =
|
||||
copy(q = q.changeWhere(f))
|
||||
|
||||
def orderBy(ob: OrderBy, obs: OrderBy*): WithCte =
|
||||
copy(q = q.orderBy(ob, obs: _*))
|
||||
|
||||
def where(c: Condition): WithCte =
|
||||
copy(q = q.where(c))
|
||||
|
||||
def withSelect(es: Nel[SelectExpr]): WithCte =
|
||||
copy(q = q.withSelect(es))
|
||||
}
|
||||
}
|
@ -0,0 +1,37 @@
|
||||
package docspell.store.qb
|
||||
|
||||
import doobie.Put
|
||||
|
||||
sealed trait SelectExpr {
|
||||
def as(alias: String): SelectExpr
|
||||
}
|
||||
|
||||
object SelectExpr {
|
||||
|
||||
case class SelectColumn(column: Column[_], alias: Option[String]) extends SelectExpr {
|
||||
def as(a: String): SelectColumn =
|
||||
copy(alias = Some(a))
|
||||
}
|
||||
|
||||
case class SelectFun(fun: DBFunction, alias: Option[String]) extends SelectExpr {
|
||||
def as(a: String): SelectFun =
|
||||
copy(alias = Some(a))
|
||||
}
|
||||
|
||||
case class SelectLit[A](value: A, alias: Option[String])(implicit val P: Put[A])
|
||||
extends SelectExpr {
|
||||
def as(a: String): SelectLit[A] =
|
||||
copy(alias = Some(a))
|
||||
}
|
||||
|
||||
case class SelectQuery(query: Select, alias: Option[String]) extends SelectExpr {
|
||||
def as(a: String): SelectQuery =
|
||||
copy(alias = Some(a))
|
||||
}
|
||||
|
||||
case class SelectCondition(cond: Condition, alias: Option[String]) extends SelectExpr {
|
||||
def as(a: String): SelectCondition =
|
||||
copy(alias = Some(a))
|
||||
}
|
||||
|
||||
}
|
18
modules/store/src/main/scala/docspell/store/qb/Setter.scala
Normal file
18
modules/store/src/main/scala/docspell/store/qb/Setter.scala
Normal file
@ -0,0 +1,18 @@
|
||||
package docspell.store.qb
|
||||
|
||||
import doobie._
|
||||
|
||||
sealed trait Setter[A]
|
||||
|
||||
object Setter {
|
||||
|
||||
case class SetOptValue[A](column: Column[A], value: Option[A])(implicit val P: Put[A])
|
||||
extends Setter[Option[A]]
|
||||
|
||||
case class SetValue[A](column: Column[A], value: A)(implicit val P: Put[A])
|
||||
extends Setter[A]
|
||||
|
||||
case class Increment[A](column: Column[A], amount: Int) extends Setter[A]
|
||||
case class Decrement[A](column: Column[A], amount: Int) extends Setter[A]
|
||||
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
package docspell.store.qb
|
||||
|
||||
trait TableDef {
|
||||
def tableName: String
|
||||
|
||||
def alias: Option[String]
|
||||
}
|
||||
|
||||
object TableDef {
|
||||
|
||||
def apply(table: String, aliasName: Option[String] = None): BasicTable =
|
||||
BasicTable(table, aliasName)
|
||||
|
||||
final case class BasicTable(tableName: String, alias: Option[String]) extends TableDef {
|
||||
def as(alias: String): BasicTable =
|
||||
copy(alias = Some(alias))
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
package docspell.store.qb.impl
|
||||
|
||||
import docspell.store.qb._
|
||||
|
||||
import doobie._
|
||||
import doobie.implicits._
|
||||
|
||||
trait CommonBuilder {
|
||||
def column(col: Column[_]): Fragment = {
|
||||
val prefix = col.table.alias.getOrElse(col.table.tableName)
|
||||
if (prefix.isEmpty) columnNoPrefix(col)
|
||||
else Fragment.const0(prefix) ++ Fragment.const0(".") ++ Fragment.const0(col.name)
|
||||
}
|
||||
|
||||
def columnNoPrefix(col: Column[_]): Fragment =
|
||||
Fragment.const0(col.name)
|
||||
|
||||
def appendAs(alias: Option[String]): Fragment =
|
||||
alias.map(a => fr" AS" ++ Fragment.const(a)).getOrElse(Fragment.empty)
|
||||
}
|
||||
object CommonBuilder extends CommonBuilder
|
@ -0,0 +1,159 @@
|
||||
package docspell.store.qb.impl
|
||||
|
||||
import cats.data.NonEmptyList
|
||||
|
||||
import docspell.store.qb._
|
||||
|
||||
import _root_.doobie.implicits._
|
||||
import _root_.doobie.{Query => _, _}
|
||||
|
||||
object ConditionBuilder {
|
||||
val or = fr" OR"
|
||||
val and = fr" AND"
|
||||
val comma = fr","
|
||||
val parenOpen = Fragment.const0("(")
|
||||
val parenClose = Fragment.const0(")")
|
||||
|
||||
final def reduce(c: Condition): Condition =
|
||||
c match {
|
||||
case Condition.And(inner) =>
|
||||
NonEmptyList.fromList(flatten(inner.toList, Condition.And.Inner)) match {
|
||||
case Some(rinner) =>
|
||||
if (rinner.tail.isEmpty) reduce(rinner.head)
|
||||
else Condition.And(rinner.reverse.map(reduce))
|
||||
case None =>
|
||||
Condition.unit
|
||||
}
|
||||
|
||||
case Condition.Or(inner) =>
|
||||
NonEmptyList.fromList(flatten(inner.toList, Condition.Or.Inner)) match {
|
||||
case Some(rinner) =>
|
||||
if (rinner.tail.isEmpty) reduce(rinner.head)
|
||||
else Condition.Or(rinner.reverse.map(reduce))
|
||||
case None =>
|
||||
Condition.unit
|
||||
}
|
||||
|
||||
case Condition.Not(Condition.UnitCondition) =>
|
||||
Condition.unit
|
||||
|
||||
case Condition.Not(Condition.Not(inner)) =>
|
||||
reduce(inner)
|
||||
|
||||
case _ =>
|
||||
c
|
||||
}
|
||||
|
||||
private def flatten(
|
||||
els: List[Condition],
|
||||
nodePattern: Condition.InnerCondition,
|
||||
result: List[Condition] = Nil
|
||||
): List[Condition] =
|
||||
els match {
|
||||
case Nil =>
|
||||
result
|
||||
case nodePattern(more) :: tail =>
|
||||
val spliced = flatten(more.toList, nodePattern, result)
|
||||
flatten(tail, nodePattern, spliced)
|
||||
case Condition.UnitCondition :: tail =>
|
||||
flatten(tail, nodePattern, result)
|
||||
case h :: tail =>
|
||||
flatten(tail, nodePattern, h :: result)
|
||||
}
|
||||
|
||||
final def build(expr: Condition): Fragment =
|
||||
reduce(expr) match {
|
||||
case c @ Condition.CompareVal(col, op, value) =>
|
||||
val opFrag = operator(op)
|
||||
val valFrag = buildValue(value)(c.P)
|
||||
val colFrag = op match {
|
||||
case Operator.LowerLike =>
|
||||
lower(col)
|
||||
case _ =>
|
||||
SelectExprBuilder.column(col)
|
||||
}
|
||||
colFrag ++ opFrag ++ valFrag
|
||||
|
||||
case c @ Condition.CompareFVal(dbf, op, value) =>
|
||||
val opFrag = operator(op)
|
||||
val valFrag = buildValue(value)(c.P)
|
||||
val dbfFrag = op match {
|
||||
case Operator.LowerLike =>
|
||||
lower(dbf)
|
||||
case _ =>
|
||||
DBFunctionBuilder.build(dbf)
|
||||
}
|
||||
dbfFrag ++ opFrag ++ valFrag
|
||||
|
||||
case Condition.CompareCol(c1, op, c2) =>
|
||||
val (c1Frag, c2Frag) = op match {
|
||||
case Operator.LowerLike =>
|
||||
(lower(c1), lower(c2))
|
||||
case _ =>
|
||||
(SelectExprBuilder.column(c1), SelectExprBuilder.column(c2))
|
||||
}
|
||||
c1Frag ++ operator(op) ++ c2Frag
|
||||
|
||||
case Condition.InSubSelect(col, subsel) =>
|
||||
val sub = SelectBuilder(subsel)
|
||||
SelectExprBuilder.column(col) ++ sql" IN (" ++ sub ++ parenClose
|
||||
|
||||
case c @ Condition.InValues(col, values, toLower) =>
|
||||
val cfrag = if (toLower) lower(col) else SelectExprBuilder.column(col)
|
||||
cfrag ++ sql" IN (" ++ values.toList
|
||||
.map(a => buildValue(a)(c.P))
|
||||
.reduce(_ ++ comma ++ _) ++ parenClose
|
||||
|
||||
case Condition.IsNull(col) =>
|
||||
SelectExprBuilder.column(col) ++ fr" is null"
|
||||
|
||||
case Condition.And(ands) =>
|
||||
val inner = ands.map(build).reduceLeft(_ ++ and ++ _)
|
||||
if (ands.tail.isEmpty) inner
|
||||
else parenOpen ++ inner ++ parenClose
|
||||
|
||||
case Condition.Or(ors) =>
|
||||
val inner = ors.map(build).reduceLeft(_ ++ or ++ _)
|
||||
if (ors.tail.isEmpty) inner
|
||||
else parenOpen ++ inner ++ parenClose
|
||||
|
||||
case Condition.Not(Condition.IsNull(col)) =>
|
||||
SelectExprBuilder.column(col) ++ fr" is not null"
|
||||
|
||||
case Condition.Not(c) =>
|
||||
fr"NOT" ++ build(c)
|
||||
|
||||
case Condition.UnitCondition =>
|
||||
Fragment.empty
|
||||
}
|
||||
|
||||
def operator(op: Operator): Fragment =
|
||||
op match {
|
||||
case Operator.Eq =>
|
||||
fr" ="
|
||||
case Operator.Neq =>
|
||||
fr" <>"
|
||||
case Operator.Gt =>
|
||||
fr" >"
|
||||
case Operator.Lt =>
|
||||
fr" <"
|
||||
case Operator.Gte =>
|
||||
fr" >="
|
||||
case Operator.Lte =>
|
||||
fr" <="
|
||||
case Operator.LowerLike =>
|
||||
fr" LIKE"
|
||||
}
|
||||
|
||||
def buildValue[A: Put](v: A): Fragment =
|
||||
fr"$v"
|
||||
|
||||
def buildOptValue[A: Put](v: Option[A]): Fragment =
|
||||
fr"$v"
|
||||
|
||||
def lower(col: Column[_]): Fragment =
|
||||
Fragment.const0("LOWER(") ++ SelectExprBuilder.column(col) ++ parenClose
|
||||
|
||||
def lower(dbf: DBFunction): Fragment =
|
||||
Fragment.const0("LOWER(") ++ DBFunctionBuilder.build(dbf) ++ parenClose
|
||||
}
|
@ -0,0 +1,61 @@
|
||||
package docspell.store.qb.impl
|
||||
|
||||
import docspell.store.qb.DBFunction
|
||||
|
||||
import doobie._
|
||||
import doobie.implicits._
|
||||
|
||||
object DBFunctionBuilder extends CommonBuilder {
|
||||
private val comma = fr","
|
||||
|
||||
def build(expr: DBFunction): Fragment =
|
||||
expr match {
|
||||
case DBFunction.CountAll =>
|
||||
sql"COUNT(*)"
|
||||
|
||||
case DBFunction.Count(col) =>
|
||||
sql"COUNT(" ++ column(col) ++ fr")"
|
||||
|
||||
case DBFunction.Max(expr) =>
|
||||
sql"MAX(" ++ SelectExprBuilder.build(expr) ++ fr")"
|
||||
|
||||
case DBFunction.Min(expr) =>
|
||||
sql"MIN(" ++ SelectExprBuilder.build(expr) ++ fr")"
|
||||
|
||||
case DBFunction.Coalesce(expr, exprs) =>
|
||||
val v = exprs.prepended(expr).map(SelectExprBuilder.build)
|
||||
sql"COALESCE(" ++ v.reduce(_ ++ comma ++ _) ++ fr")"
|
||||
|
||||
case DBFunction.Power(expr, base) =>
|
||||
sql"POWER($base, " ++ SelectExprBuilder.build(expr) ++ fr")"
|
||||
|
||||
case DBFunction.Substring(expr, start, len) =>
|
||||
sql"SUBSTRING(" ++ SelectExprBuilder.build(expr) ++ fr" FROM $start FOR $len)"
|
||||
|
||||
case DBFunction.Calc(op, left, right) =>
|
||||
SelectExprBuilder.build(left) ++
|
||||
buildOperator(op) ++
|
||||
SelectExprBuilder.build(right)
|
||||
|
||||
case DBFunction.Cast(f, newType) =>
|
||||
sql"CAST(" ++ SelectExprBuilder.build(f) ++
|
||||
fr" AS" ++ Fragment.const(newType) ++
|
||||
sql")"
|
||||
|
||||
case DBFunction.Avg(expr) =>
|
||||
sql"AVG(" ++ SelectExprBuilder.build(expr) ++ fr")"
|
||||
|
||||
case DBFunction.Sum(expr) =>
|
||||
sql"SUM(" ++ SelectExprBuilder.build(expr) ++ fr")"
|
||||
}
|
||||
|
||||
def buildOperator(op: DBFunction.Operator): Fragment =
|
||||
op match {
|
||||
case DBFunction.Operator.Minus =>
|
||||
fr" -"
|
||||
case DBFunction.Operator.Plus =>
|
||||
fr" +"
|
||||
case DBFunction.Operator.Mult =>
|
||||
fr" *"
|
||||
}
|
||||
}
|
@ -0,0 +1,45 @@
|
||||
package docspell.store.qb.impl
|
||||
|
||||
import docspell.store.qb._
|
||||
|
||||
import _root_.doobie.implicits._
|
||||
import _root_.doobie.{Query => _, _}
|
||||
|
||||
object FromExprBuilder {
|
||||
|
||||
def build(expr: FromExpr): Fragment =
|
||||
expr match {
|
||||
case FromExpr.From(relation) =>
|
||||
fr" FROM" ++ buildRelation(relation)
|
||||
|
||||
case FromExpr.Joined(from, joins) =>
|
||||
build(from) ++
|
||||
joins.map(buildJoin).foldLeft(Fragment.empty)(_ ++ _)
|
||||
}
|
||||
|
||||
def buildTable(table: TableDef): Fragment =
|
||||
Fragment.const(table.tableName) ++ table.alias
|
||||
.map(a => Fragment.const0(a))
|
||||
.getOrElse(Fragment.empty)
|
||||
|
||||
def buildRelation(rel: FromExpr.Relation): Fragment =
|
||||
rel match {
|
||||
case FromExpr.Relation.Table(table) =>
|
||||
buildTable(table)
|
||||
|
||||
case FromExpr.Relation.SubSelect(sel, alias) =>
|
||||
sql" (" ++ SelectBuilder(sel) ++ fr") AS" ++ Fragment.const(alias)
|
||||
}
|
||||
|
||||
def buildJoin(join: FromExpr.Join): Fragment =
|
||||
join match {
|
||||
case FromExpr.Join.InnerJoin(table, cond) =>
|
||||
val c = fr" ON" ++ ConditionBuilder.build(cond)
|
||||
fr" INNER JOIN" ++ buildRelation(table) ++ c
|
||||
|
||||
case FromExpr.Join.LeftJoin(table, cond) =>
|
||||
val c = fr" ON" ++ ConditionBuilder.build(cond)
|
||||
fr" LEFT JOIN" ++ buildRelation(table) ++ c
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,114 @@
|
||||
package docspell.store.qb.impl
|
||||
|
||||
import cats.data.NonEmptyList
|
||||
|
||||
import docspell.store.qb._
|
||||
|
||||
import _root_.doobie.implicits._
|
||||
import _root_.doobie.{Query => _, _}
|
||||
|
||||
object SelectBuilder {
|
||||
val comma = fr","
|
||||
val asc = fr" ASC"
|
||||
val desc = fr" DESC"
|
||||
val intersect = fr" INTERSECT"
|
||||
val union = fr" UNION ALL"
|
||||
|
||||
def apply(q: Select): Fragment =
|
||||
build(q)
|
||||
|
||||
def build(q: Select): Fragment =
|
||||
q match {
|
||||
case sq: Select.SimpleSelect =>
|
||||
val sel = if (sq.distinctFlag) fr"SELECT DISTINCT" else fr"SELECT"
|
||||
sel ++ buildSimple(sq)
|
||||
|
||||
case Select.RawSelect(f) =>
|
||||
f
|
||||
|
||||
case Select.Union(q, qs) =>
|
||||
qs.prepended(q).map(build).reduce(_ ++ union ++ _)
|
||||
|
||||
case Select.Intersect(q, qs) =>
|
||||
qs.prepended(q).map(build).reduce(_ ++ intersect ++ _)
|
||||
|
||||
case Select.Ordered(q, ob, obs) =>
|
||||
val order = obs.prepended(ob).map(orderBy).reduce(_ ++ comma ++ _)
|
||||
build(q) ++ fr" ORDER BY" ++ order
|
||||
|
||||
case Select.Limit(q, batch) =>
|
||||
build(q) ++ buildBatch(batch)
|
||||
|
||||
case Select.WithCte(cte, moreCte, query) =>
|
||||
val ctes = moreCte.prepended(cte)
|
||||
fr"WITH" ++ ctes.map(buildCte).reduce(_ ++ comma ++ _) ++ fr" " ++ build(query)
|
||||
}
|
||||
|
||||
def buildSimple(sq: Select.SimpleSelect): Fragment = {
|
||||
val f0 = sq.projection.map(selectExpr).reduceLeft(_ ++ comma ++ _)
|
||||
val f1 = fromExpr(sq.from)
|
||||
val f2 = cond(sq.where)
|
||||
val f3 = sq.groupBy.map(groupBy).getOrElse(Fragment.empty)
|
||||
f0 ++ f1 ++ f2 ++ f3
|
||||
}
|
||||
|
||||
def orderBy(ob: OrderBy): Fragment = {
|
||||
val f1 = selectExpr(ob.expr)
|
||||
val f2 = ob.orderType match {
|
||||
case OrderBy.OrderType.Asc =>
|
||||
asc
|
||||
case OrderBy.OrderType.Desc =>
|
||||
desc
|
||||
}
|
||||
f1 ++ f2
|
||||
}
|
||||
|
||||
def selectExpr(se: SelectExpr): Fragment =
|
||||
SelectExprBuilder.build(se)
|
||||
|
||||
def fromExpr(fr: FromExpr): Fragment =
|
||||
FromExprBuilder.build(fr)
|
||||
|
||||
def cond(c: Condition): Fragment =
|
||||
c match {
|
||||
case Condition.UnitCondition =>
|
||||
Fragment.empty
|
||||
case _ =>
|
||||
fr" WHERE" ++ ConditionBuilder.build(c)
|
||||
}
|
||||
|
||||
def groupBy(gb: GroupBy): Fragment = {
|
||||
val f0 = gb.names.prepended(gb.name).map(selectExpr).reduce(_ ++ comma ++ _)
|
||||
val f1 = gb.having.map(cond).getOrElse(Fragment.empty)
|
||||
fr"GROUP BY" ++ f0 ++ f1
|
||||
}
|
||||
|
||||
def buildCte(bind: CteBind): Fragment =
|
||||
bind match {
|
||||
case CteBind(name, cols, select) =>
|
||||
val colDef =
|
||||
NonEmptyList
|
||||
.fromFoldable(cols)
|
||||
.map(nel =>
|
||||
nel
|
||||
.map(col => CommonBuilder.columnNoPrefix(col))
|
||||
.reduceLeft(_ ++ comma ++ _)
|
||||
)
|
||||
.map(f => sql"(" ++ f ++ sql")")
|
||||
.getOrElse(Fragment.empty)
|
||||
|
||||
Fragment.const0(name.tableName) ++ colDef ++ sql" AS (" ++ build(select) ++ sql")"
|
||||
}
|
||||
|
||||
def buildBatch(b: Batch): Fragment = {
|
||||
val limitFrag =
|
||||
if (b.limit != Int.MaxValue) fr" LIMIT ${b.limit}"
|
||||
else Fragment.empty
|
||||
|
||||
val offsetFrag =
|
||||
if (b.offset != 0) fr" OFFSET ${b.offset}"
|
||||
else Fragment.empty
|
||||
|
||||
limitFrag ++ offsetFrag
|
||||
}
|
||||
}
|
@ -0,0 +1,28 @@
|
||||
package docspell.store.qb.impl
|
||||
|
||||
import docspell.store.qb._
|
||||
|
||||
import doobie._
|
||||
import doobie.implicits._
|
||||
|
||||
object SelectExprBuilder extends CommonBuilder {
|
||||
|
||||
def build(expr: SelectExpr): Fragment =
|
||||
expr match {
|
||||
case SelectExpr.SelectColumn(col, alias) =>
|
||||
column(col) ++ appendAs(alias)
|
||||
|
||||
case s @ SelectExpr.SelectLit(value, aliasOpt) =>
|
||||
ConditionBuilder.buildValue(value)(s.P) ++ appendAs(aliasOpt)
|
||||
|
||||
case SelectExpr.SelectFun(fun, alias) =>
|
||||
DBFunctionBuilder.build(fun) ++ appendAs(alias)
|
||||
|
||||
case SelectExpr.SelectQuery(query, alias) =>
|
||||
sql"(" ++ SelectBuilder.build(query) ++ sql")" ++ appendAs(alias)
|
||||
|
||||
case SelectExpr.SelectCondition(cond, alias) =>
|
||||
sql"(" ++ ConditionBuilder.build(cond) ++ sql")" ++ appendAs(alias)
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
package docspell.store.queries
|
||||
|
||||
import docspell.common._
|
||||
|
||||
case class AttachmentLight(
|
||||
id: Ident,
|
||||
position: Int,
|
||||
name: Option[String],
|
||||
pageCount: Option[Int]
|
||||
)
|
@ -0,0 +1,5 @@
|
||||
package docspell.store.queries
|
||||
|
||||
import docspell.common._
|
||||
|
||||
case class CustomValue(field: Ident, value: String)
|
@ -0,0 +1,17 @@
|
||||
package docspell.store.queries
|
||||
|
||||
import docspell.store.records.RCustomField
|
||||
|
||||
case class FieldStats(
|
||||
field: RCustomField,
|
||||
count: Int,
|
||||
avg: BigDecimal,
|
||||
sum: BigDecimal,
|
||||
max: BigDecimal,
|
||||
min: BigDecimal
|
||||
)
|
||||
|
||||
object FieldStats {
|
||||
def apply(field: RCustomField, count: Int): FieldStats =
|
||||
FieldStats(field, count, BigDecimal(0), BigDecimal(0), BigDecimal(0), BigDecimal(0))
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
package docspell.store.queries
|
||||
|
||||
import docspell.common._
|
||||
|
||||
case class FolderCount(id: Ident, name: String, owner: IdRef, count: Int)
|
@ -0,0 +1,25 @@
|
||||
package docspell.store.queries
|
||||
|
||||
import docspell.common._
|
||||
import docspell.store.records._
|
||||
|
||||
import bitpeace.FileMeta
|
||||
|
||||
case class ItemData(
|
||||
item: RItem,
|
||||
corrOrg: Option[ROrganization],
|
||||
corrPerson: Option[RPerson],
|
||||
concPerson: Option[RPerson],
|
||||
concEquip: Option[REquipment],
|
||||
inReplyTo: Option[IdRef],
|
||||
folder: Option[IdRef],
|
||||
tags: Vector[RTag],
|
||||
attachments: Vector[(RAttachment, FileMeta)],
|
||||
sources: Vector[(RAttachmentSource, FileMeta)],
|
||||
archives: Vector[(RAttachmentArchive, FileMeta)],
|
||||
customFields: Vector[ItemFieldValue]
|
||||
) {
|
||||
|
||||
def filterCollective(coll: Ident): Option[ItemData] =
|
||||
if (item.cid == coll) Some(this) else None
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
package docspell.store.queries
|
||||
|
||||
import docspell.common._
|
||||
|
||||
case class ItemFieldValue(
|
||||
fieldId: Ident,
|
||||
fieldName: Ident,
|
||||
fieldLabel: Option[String],
|
||||
fieldType: CustomFieldType,
|
||||
value: String
|
||||
)
|
@ -0,0 +1,21 @@
|
||||
package docspell.store.queries
|
||||
|
||||
import docspell.common._
|
||||
|
||||
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],
|
||||
folder: Option[IdRef],
|
||||
notes: Option[String]
|
||||
)
|
@ -0,0 +1,10 @@
|
||||
package docspell.store.queries
|
||||
|
||||
import docspell.store.records.RTag
|
||||
|
||||
case class ListItemWithTags(
|
||||
item: ListItem,
|
||||
tags: List[RTag],
|
||||
attachments: List[AttachmentLight],
|
||||
customfields: List[ItemFieldValue]
|
||||
)
|
@ -8,15 +8,20 @@ import fs2.Stream
|
||||
import docspell.common._
|
||||
import docspell.common.syntax.all._
|
||||
import docspell.store.Store
|
||||
import docspell.store.impl.Implicits._
|
||||
import docspell.store.qb.DSL._
|
||||
import docspell.store.qb._
|
||||
import docspell.store.records._
|
||||
|
||||
import doobie._
|
||||
import doobie.implicits._
|
||||
|
||||
object QAttachment {
|
||||
private[this] val logger = org.log4s.getLogger
|
||||
|
||||
private val a = RAttachment.as("a")
|
||||
private val item = RItem.as("i")
|
||||
private val am = RAttachmentMeta.as("am")
|
||||
private val c = RCollective.as("c")
|
||||
|
||||
def deletePreview[F[_]: Sync](store: Store[F])(attachId: Ident): F[Int] = {
|
||||
val findPreview =
|
||||
for {
|
||||
@ -113,20 +118,13 @@ object QAttachment {
|
||||
} yield ns.sum
|
||||
|
||||
def getMetaProposals(itemId: Ident, coll: Ident): ConnectionIO[MetaProposalList] = {
|
||||
val AC = RAttachment.Columns
|
||||
val MC = RAttachmentMeta.Columns
|
||||
val IC = RItem.Columns
|
||||
|
||||
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" ++ 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))
|
||||
val q = Select(
|
||||
am.proposals.s,
|
||||
from(am)
|
||||
.innerJoin(a, a.id === am.id)
|
||||
.innerJoin(item, a.itemId === item.id),
|
||||
a.itemId === itemId && item.cid === coll
|
||||
).build
|
||||
|
||||
for {
|
||||
ml <- q.query[MetaProposalList].to[Vector]
|
||||
@ -137,24 +135,13 @@ object QAttachment {
|
||||
attachId: Ident,
|
||||
collective: Ident
|
||||
): ConnectionIO[Option[RAttachmentMeta]] = {
|
||||
val AC = RAttachment.Columns
|
||||
val MC = RAttachmentMeta.Columns
|
||||
val IC = RItem.Columns
|
||||
|
||||
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" ++ 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)
|
||||
)
|
||||
val q = Select(
|
||||
select(am.all),
|
||||
from(item)
|
||||
.innerJoin(a, a.itemId === item.id)
|
||||
.innerJoin(am, am.id === a.id),
|
||||
a.id === attachId && item.cid === collective
|
||||
).build
|
||||
|
||||
q.query[RAttachmentMeta].option
|
||||
}
|
||||
@ -171,28 +158,16 @@ object QAttachment {
|
||||
def allAttachmentMetaAndName(
|
||||
coll: Option[Ident],
|
||||
chunkSize: Int
|
||||
): Stream[ConnectionIO, ContentAndName] = {
|
||||
val aId = RAttachment.Columns.id.prefix("a")
|
||||
val aItem = RAttachment.Columns.itemId.prefix("a")
|
||||
val aName = RAttachment.Columns.name.prefix("a")
|
||||
val mId = RAttachmentMeta.Columns.id.prefix("m")
|
||||
val mContent = RAttachmentMeta.Columns.content.prefix("m")
|
||||
val iId = RItem.Columns.id.prefix("i")
|
||||
val iColl = RItem.Columns.cid.prefix("i")
|
||||
val iFolder = RItem.Columns.folder.prefix("i")
|
||||
val cId = RCollective.Columns.id.prefix("c")
|
||||
val cLang = RCollective.Columns.language.prefix("c")
|
||||
|
||||
val cols = Seq(aId, aItem, iColl, iFolder, cLang, aName, mContent)
|
||||
val from = RAttachment.table ++ fr"a INNER JOIN" ++
|
||||
RAttachmentMeta.table ++ fr"m ON" ++ aId.is(mId) ++
|
||||
fr"INNER JOIN" ++ RItem.table ++ fr"i ON" ++ iId.is(aItem) ++
|
||||
fr"INNER JOIN" ++ RCollective.table ++ fr"c ON" ++ cId.is(iColl)
|
||||
|
||||
val where = coll.map(cid => iColl.is(cid)).getOrElse(Fragment.empty)
|
||||
|
||||
selectSimple(cols, from, where)
|
||||
): Stream[ConnectionIO, ContentAndName] =
|
||||
Select(
|
||||
select(a.id, a.itemId, item.cid, item.folder, c.language, a.name, am.content),
|
||||
from(a)
|
||||
.innerJoin(am, am.id === a.id)
|
||||
.innerJoin(item, item.id === a.itemId)
|
||||
.innerJoin(c, c.id === item.cid)
|
||||
).where(coll.map(cid => item.cid === cid))
|
||||
.build
|
||||
.query[ContentAndName]
|
||||
.streamWithChunkSize(chunkSize)
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -5,13 +5,20 @@ import fs2.Stream
|
||||
|
||||
import docspell.common.ContactKind
|
||||
import docspell.common.{Direction, Ident}
|
||||
import docspell.store.impl.Implicits._
|
||||
import docspell.store.qb.DSL._
|
||||
import docspell.store.qb._
|
||||
import docspell.store.records._
|
||||
|
||||
import doobie._
|
||||
import doobie.implicits._
|
||||
|
||||
object QCollective {
|
||||
private val ti = RTagItem.as("ti")
|
||||
private val t = RTag.as("t")
|
||||
private val ro = ROrganization.as("o")
|
||||
private val rp = RPerson.as("p")
|
||||
private val rc = RContact.as("c")
|
||||
private val i = RItem.as("i")
|
||||
|
||||
case class Names(org: Vector[String], pers: Vector[String], equip: Vector[String])
|
||||
object Names {
|
||||
@ -26,8 +33,6 @@ object QCollective {
|
||||
} yield Names(orgs.map(_.name), pers.map(_.name), equp.map(_.name)))
|
||||
.getOrElse(Names.empty)
|
||||
|
||||
case class TagCount(tag: RTag, count: Int)
|
||||
|
||||
case class InsightData(
|
||||
incoming: Int,
|
||||
outgoing: Int,
|
||||
@ -36,17 +41,16 @@ object QCollective {
|
||||
)
|
||||
|
||||
def getInsights(coll: Ident): ConnectionIO[InsightData] = {
|
||||
val IC = RItem.Columns
|
||||
val q0 = selectCount(
|
||||
IC.id,
|
||||
RItem.table,
|
||||
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 q0 = Select(
|
||||
count(i.id).s,
|
||||
from(i),
|
||||
i.cid === coll && i.incoming === Direction.incoming
|
||||
).build.query[Int].unique
|
||||
val q1 = Select(
|
||||
count(i.id).s,
|
||||
from(i),
|
||||
i.cid === coll && i.incoming === Direction.outgoing
|
||||
).build.query[Int].unique
|
||||
|
||||
val fileSize = sql"""
|
||||
select sum(length) from (
|
||||
@ -77,24 +81,14 @@ object QCollective {
|
||||
}
|
||||
|
||||
def tagCloud(coll: Ident): ConnectionIO[List[TagCount]] = {
|
||||
val TC = RTag.Columns
|
||||
val RC = RTagItem.Columns
|
||||
val sql =
|
||||
Select(
|
||||
select(t.all).append(count(ti.itemId).s),
|
||||
from(ti).innerJoin(t, ti.tagId === t.tid),
|
||||
t.cid === coll
|
||||
).groupBy(t.name, t.tid, t.category)
|
||||
|
||||
val q3 = fr"SELECT" ++ commas(
|
||||
TC.all.map(_.prefix("t").f) ++ Seq(fr"count(" ++ RC.itemId.prefix("r").f ++ fr")")
|
||||
) ++
|
||||
fr"FROM" ++ RTagItem.table ++ fr"r" ++
|
||||
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"GROUP BY" ++ commas(
|
||||
TC.name.prefix("t").f,
|
||||
TC.tid.prefix("t").f,
|
||||
TC.category.prefix("t").f
|
||||
)
|
||||
|
||||
q3.query[TagCount].to[List]
|
||||
sql.build.query[TagCount].to[List]
|
||||
}
|
||||
|
||||
def getContacts(
|
||||
@ -102,35 +96,15 @@ object QCollective {
|
||||
query: Option[String],
|
||||
kind: Option[ContactKind]
|
||||
): Stream[ConnectionIO, RContact] = {
|
||||
val RO = ROrganization
|
||||
val RP = RPerson
|
||||
val RC = RContact
|
||||
val orgCond = Select(select(ro.oid), from(ro), ro.cid === coll)
|
||||
val persCond = Select(select(rp.pid), from(rp), rp.cid === coll)
|
||||
val valueFilter = query.map(s => rc.value.like(s"%${s.toLowerCase}%"))
|
||||
val kindFilter = kind.map(k => rc.kind === k)
|
||||
|
||||
val orgCond = selectSimple(Seq(RO.Columns.oid), RO.table, RO.Columns.cid.is(coll))
|
||||
val persCond = selectSimple(Seq(RP.Columns.pid), RP.table, RP.Columns.cid.is(coll))
|
||||
val queryCond = query match {
|
||||
case Some(q) =>
|
||||
Seq(RC.Columns.value.lowerLike(s"%${q.toLowerCase}%"))
|
||||
case None =>
|
||||
Seq.empty
|
||||
}
|
||||
val kindCond = kind match {
|
||||
case Some(k) =>
|
||||
Seq(RC.Columns.kind.is(k))
|
||||
case None =>
|
||||
Seq.empty
|
||||
}
|
||||
|
||||
val q = selectSimple(
|
||||
RC.Columns.all,
|
||||
RC.table,
|
||||
and(
|
||||
Seq(
|
||||
or(RC.Columns.orgId.isIn(orgCond), RC.Columns.personId.isIn(persCond))
|
||||
) ++ queryCond ++ kindCond
|
||||
)
|
||||
) ++ orderBy(RC.Columns.value.f)
|
||||
|
||||
q.query[RContact].stream
|
||||
Select(
|
||||
select(rc.all),
|
||||
from(rc),
|
||||
(rc.orgId.in(orgCond) || rc.personId.in(persCond)) &&? valueFilter &&? kindFilter
|
||||
).orderBy(rc.value).build.query[RContact].stream
|
||||
}
|
||||
}
|
||||
|
@ -1,17 +1,17 @@
|
||||
package docspell.store.queries
|
||||
|
||||
import cats.data.NonEmptyList
|
||||
import cats.implicits._
|
||||
|
||||
import docspell.common._
|
||||
import docspell.store.impl.Column
|
||||
import docspell.store.impl.Implicits._
|
||||
import docspell.store.qb.DSL._
|
||||
import docspell.store.qb._
|
||||
import docspell.store.records._
|
||||
|
||||
import doobie._
|
||||
import doobie.implicits._
|
||||
|
||||
object QCustomField {
|
||||
private val f = RCustomField.as("f")
|
||||
private val v = RCustomFieldValue.as("v")
|
||||
|
||||
case class CustomFieldData(field: RCustomField, usageCount: Int)
|
||||
|
||||
@ -19,46 +19,26 @@ object QCustomField {
|
||||
coll: Ident,
|
||||
nameQuery: Option[String]
|
||||
): ConnectionIO[Vector[CustomFieldData]] =
|
||||
findFragment(coll, nameQuery, None).query[CustomFieldData].to[Vector]
|
||||
findFragment(coll, nameQuery, None).build.query[CustomFieldData].to[Vector]
|
||||
|
||||
def findById(field: Ident, collective: Ident): ConnectionIO[Option[CustomFieldData]] =
|
||||
findFragment(collective, None, field.some).query[CustomFieldData].option
|
||||
findFragment(collective, None, field.some).build.query[CustomFieldData].option
|
||||
|
||||
private def findFragment(
|
||||
coll: Ident,
|
||||
nameQuery: Option[String],
|
||||
fieldId: Option[Ident]
|
||||
): Fragment = {
|
||||
val fId = RCustomField.Columns.id.prefix("f")
|
||||
val fColl = RCustomField.Columns.cid.prefix("f")
|
||||
val fName = RCustomField.Columns.name.prefix("f")
|
||||
val fLabel = RCustomField.Columns.label.prefix("f")
|
||||
val vField = RCustomFieldValue.Columns.field.prefix("v")
|
||||
|
||||
val join = RCustomField.table ++ fr"f LEFT OUTER JOIN" ++
|
||||
RCustomFieldValue.table ++ fr"v ON" ++ fId.is(vField)
|
||||
|
||||
val cols = RCustomField.Columns.all.map(_.prefix("f")) :+ Column("COUNT(v.id)")
|
||||
|
||||
val nameCond = nameQuery.map(QueryWildcard.apply) match {
|
||||
case Some(q) =>
|
||||
or(fName.lowerLike(q), and(fLabel.isNotNull, fLabel.lowerLike(q)))
|
||||
case None =>
|
||||
Fragment.empty
|
||||
}
|
||||
val fieldCond = fieldId match {
|
||||
case Some(id) =>
|
||||
fId.is(id)
|
||||
case None =>
|
||||
Fragment.empty
|
||||
}
|
||||
val cond = and(fColl.is(coll), nameCond, fieldCond)
|
||||
|
||||
val group = NonEmptyList.fromList(RCustomField.Columns.all) match {
|
||||
case Some(nel) => groupBy(nel.map(_.prefix("f")))
|
||||
case None => Fragment.empty
|
||||
): Select = {
|
||||
val nameFilter = nameQuery.map { q =>
|
||||
f.name.likes(q) || (f.label.isNotNull && f.label.like(q))
|
||||
}
|
||||
|
||||
selectSimple(cols, join, cond) ++ group
|
||||
Select(
|
||||
f.all.map(_.s).append(count(v.id).as("num")),
|
||||
from(f)
|
||||
.leftJoin(v, f.id === v.field),
|
||||
f.cid === coll &&? nameFilter &&? fieldId.map(fid => f.id === fid),
|
||||
GroupBy(f.all)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -4,7 +4,8 @@ import cats.data.OptionT
|
||||
import cats.implicits._
|
||||
|
||||
import docspell.common._
|
||||
import docspell.store.impl.Implicits._
|
||||
import docspell.store.qb.DSL._
|
||||
import docspell.store.qb._
|
||||
import docspell.store.records._
|
||||
|
||||
import doobie._
|
||||
@ -136,21 +137,16 @@ object QFolder {
|
||||
}
|
||||
|
||||
def findById(id: Ident, account: AccountId): ConnectionIO[Option[FolderDetail]] = {
|
||||
val mUserId = RFolderMember.Columns.user.prefix("m")
|
||||
val mFolderId = RFolderMember.Columns.folder.prefix("m")
|
||||
val uId = RUser.Columns.uid.prefix("u")
|
||||
val uLogin = RUser.Columns.login.prefix("u")
|
||||
val sColl = RFolder.Columns.collective.prefix("s")
|
||||
val sId = RFolder.Columns.id.prefix("s")
|
||||
val user = RUser.as("u")
|
||||
val member = RFolderMember.as("m")
|
||||
val folder = RFolder.as("s")
|
||||
|
||||
val from = RFolderMember.table ++ fr"m INNER JOIN" ++
|
||||
RUser.table ++ fr"u ON" ++ mUserId.is(uId) ++ fr"INNER JOIN" ++
|
||||
RFolder.table ++ fr"s ON" ++ mFolderId.is(sId)
|
||||
|
||||
val memberQ = selectSimple(
|
||||
Seq(uId, uLogin),
|
||||
from,
|
||||
and(mFolderId.is(id), sColl.is(account.collective))
|
||||
val memberQ = run(
|
||||
select(user.uid, user.login),
|
||||
from(member)
|
||||
.innerJoin(user, member.user === user.uid)
|
||||
.innerJoin(folder, member.folder === folder.id),
|
||||
member.folder === id && folder.collective === account.collective
|
||||
).query[IdRef].to[Vector]
|
||||
|
||||
(for {
|
||||
@ -187,92 +183,83 @@ object QFolder {
|
||||
// inner join user_ u on u.uid = s.owner
|
||||
// where s.cid = 'eike';
|
||||
|
||||
val uId = RUser.Columns.uid.prefix("u")
|
||||
val uLogin = RUser.Columns.login.prefix("u")
|
||||
val sId = RFolder.Columns.id.prefix("s")
|
||||
val sOwner = RFolder.Columns.owner.prefix("s")
|
||||
val sName = RFolder.Columns.name.prefix("s")
|
||||
val sColl = RFolder.Columns.collective.prefix("s")
|
||||
val mUser = RFolderMember.Columns.user.prefix("m")
|
||||
val mFolder = RFolderMember.Columns.folder.prefix("m")
|
||||
val user = RUser.as("u")
|
||||
val member = RFolderMember.as("m")
|
||||
val folder = RFolder.as("s")
|
||||
val memlogin = TableDef("memberlogin")
|
||||
val mlFolder = Column[Ident]("folder", memlogin)
|
||||
val mlLogin = Column[Ident]("login", memlogin)
|
||||
|
||||
//CTE
|
||||
val cte: Fragment = {
|
||||
val from1 = RFolderMember.table ++ fr"m INNER JOIN" ++
|
||||
RUser.table ++ fr"u ON" ++ uId.is(mUser) ++ fr"INNER JOIN" ++
|
||||
RFolder.table ++ fr"s ON" ++ sId.is(mFolder)
|
||||
|
||||
val from2 = RFolder.table ++ fr"s INNER JOIN" ++
|
||||
RUser.table ++ fr"u ON" ++ uId.is(sOwner)
|
||||
|
||||
withCTE(
|
||||
"memberlogin" ->
|
||||
(selectSimple(Seq(mFolder, uLogin), from1, sColl.is(account.collective)) ++
|
||||
fr"UNION ALL" ++
|
||||
selectSimple(Seq(sId, uLogin), from2, sColl.is(account.collective)))
|
||||
withCte(
|
||||
memlogin -> union(
|
||||
Select(
|
||||
select(member.folder.as(mlFolder), user.login.as(mlLogin)),
|
||||
from(member)
|
||||
.innerJoin(user, user.uid === member.user)
|
||||
.innerJoin(folder, folder.id === member.folder),
|
||||
folder.collective === account.collective
|
||||
),
|
||||
Select(
|
||||
select(folder.id.as(mlFolder), user.login.as(mlLogin)),
|
||||
from(folder)
|
||||
.innerJoin(user, user.uid === folder.owner),
|
||||
folder.collective === account.collective
|
||||
)
|
||||
}
|
||||
|
||||
val isMember =
|
||||
fr"SELECT COUNT(*) > 0 FROM memberlogin WHERE" ++ mFolder.prefix("").is(sId) ++
|
||||
fr"AND" ++ uLogin.prefix("").is(account.user)
|
||||
|
||||
val memberCount =
|
||||
fr"SELECT COUNT(*) - 1 FROM memberlogin WHERE" ++ mFolder.prefix("").is(sId)
|
||||
|
||||
//Query
|
||||
val cols = Seq(
|
||||
sId.f,
|
||||
sName.f,
|
||||
sOwner.f,
|
||||
uLogin.f,
|
||||
RFolder.Columns.created.prefix("s").f,
|
||||
fr"(" ++ isMember ++ fr") as mem",
|
||||
fr"(" ++ memberCount ++ fr") as cnt"
|
||||
)
|
||||
|
||||
val from = RFolder.table ++ fr"s INNER JOIN" ++
|
||||
RUser.table ++ fr"u ON" ++ uId.is(sOwner)
|
||||
|
||||
val where =
|
||||
sColl.is(account.collective) :: idQ.toList
|
||||
.map(id => sId.is(id)) ::: nameQ.toList.map(q =>
|
||||
sName.lowerLike(s"%${q.toLowerCase}%")
|
||||
) ::: ownerLogin.toList.map(login => uLogin.is(login))
|
||||
|
||||
(cte ++ selectSimple(commas(cols), from, and(where) ++ orderBy(sName.asc)))
|
||||
.query[FolderItem]
|
||||
.to[Vector]
|
||||
)(
|
||||
Select(
|
||||
select(
|
||||
folder.id.s,
|
||||
folder.name.s,
|
||||
folder.owner.s,
|
||||
user.login.s,
|
||||
folder.created.s,
|
||||
Select(
|
||||
select(countAll > 0),
|
||||
from(memlogin),
|
||||
mlFolder === folder.id && mlLogin === account.user
|
||||
).as("member"),
|
||||
Select(
|
||||
select(countAll - 1),
|
||||
from(memlogin),
|
||||
mlFolder === folder.id
|
||||
).as("member_count")
|
||||
),
|
||||
from(folder)
|
||||
.innerJoin(user, user.uid === folder.owner),
|
||||
where(
|
||||
folder.collective === account.collective &&?
|
||||
idQ.map(id => folder.id === id) &&?
|
||||
nameQ.map(q => folder.name.like(s"%${q.toLowerCase}%")) &&?
|
||||
ownerLogin.map(login => user.login === login)
|
||||
)
|
||||
).orderBy(folder.name.asc)
|
||||
).build.query[FolderItem].to[Vector]
|
||||
}
|
||||
|
||||
/** Select all folder_id where the given account is member or owner. */
|
||||
def findMemberFolderIds(account: AccountId): Fragment = {
|
||||
val fId = RFolder.Columns.id.prefix("f")
|
||||
val fOwner = RFolder.Columns.owner.prefix("f")
|
||||
val fColl = RFolder.Columns.collective.prefix("f")
|
||||
val uId = RUser.Columns.uid.prefix("u")
|
||||
val uLogin = RUser.Columns.login.prefix("u")
|
||||
val mFolder = RFolderMember.Columns.folder.prefix("m")
|
||||
val mUser = RFolderMember.Columns.user.prefix("m")
|
||||
|
||||
selectSimple(
|
||||
Seq(fId),
|
||||
RFolder.table ++ fr"f INNER JOIN" ++ RUser.table ++ fr"u ON" ++ fOwner.is(uId),
|
||||
and(fColl.is(account.collective), uLogin.is(account.user))
|
||||
) ++
|
||||
fr"UNION ALL" ++
|
||||
selectSimple(
|
||||
Seq(mFolder),
|
||||
RFolderMember.table ++ fr"m INNER JOIN" ++ RFolder.table ++ fr"f ON" ++ fId.is(
|
||||
mFolder
|
||||
) ++
|
||||
fr"INNER JOIN" ++ RUser.table ++ fr"u ON" ++ uId.is(mUser),
|
||||
and(fColl.is(account.collective), uLogin.is(account.user))
|
||||
def findMemberFolderIds(account: AccountId): Select = {
|
||||
val user = RUser.as("u")
|
||||
val f = RFolder.as("f")
|
||||
val m = RFolderMember.as("m")
|
||||
union(
|
||||
Select(
|
||||
select(f.id),
|
||||
from(f).innerJoin(user, f.owner === user.uid),
|
||||
f.collective === account.collective && user.login === account.user
|
||||
),
|
||||
Select(
|
||||
select(m.folder),
|
||||
from(m)
|
||||
.innerJoin(f, f.id === m.folder)
|
||||
.innerJoin(user, user.uid === m.user),
|
||||
f.collective === account.collective && user.login === account.user
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
def getMemberFolders(account: AccountId): ConnectionIO[Set[Ident]] =
|
||||
findMemberFolderIds(account).query[Ident].to[Set]
|
||||
findMemberFolderIds(account).build.query[Ident].to[Set]
|
||||
|
||||
private def findUserId(account: AccountId): ConnectionIO[Option[Ident]] =
|
||||
RUser.findByAccount(account).map(_.map(_.uid))
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -1,5 +1,6 @@
|
||||
package docspell.store.queries
|
||||
|
||||
import cats.data.NonEmptyList
|
||||
import cats.effect.Effect
|
||||
import cats.implicits._
|
||||
import fs2.Stream
|
||||
@ -7,7 +8,8 @@ import fs2.Stream
|
||||
import docspell.common._
|
||||
import docspell.common.syntax.all._
|
||||
import docspell.store.Store
|
||||
import docspell.store.impl.Implicits._
|
||||
import docspell.store.qb.DSL._
|
||||
import docspell.store.qb._
|
||||
import docspell.store.records.{RJob, RJobGroupUse, RJobLog}
|
||||
|
||||
import doobie._
|
||||
@ -89,70 +91,60 @@ object QJob {
|
||||
now: Timestamp,
|
||||
initialPause: Duration
|
||||
): ConnectionIO[Option[Ident]] = {
|
||||
val JC = RJob.Columns
|
||||
val waiting: JobState = JobState.Waiting
|
||||
val stuck: JobState = JobState.Stuck
|
||||
val jgroup = JC.group.prefix("a")
|
||||
val jstate = JC.state.prefix("a")
|
||||
val ugroup = RJobGroupUse.Columns.group.prefix("b")
|
||||
val uworker = RJobGroupUse.Columns.worker.prefix("b")
|
||||
|
||||
val stuckTrigger = coalesce(JC.startedmillis.prefix("a").f, sql"${now.toMillis}") ++
|
||||
fr"+" ++ power2(JC.retries.prefix("a")) ++ fr"* ${initialPause.millis}"
|
||||
val JC = RJob.as("a")
|
||||
val G = RJobGroupUse.as("b")
|
||||
|
||||
val stuckTrigger = stuckTriggerValue(JC, initialPause, now)
|
||||
val stateCond =
|
||||
or(jstate.is(waiting), and(jstate.is(stuck), stuckTrigger ++ fr"< ${now.toMillis}"))
|
||||
JC.state === JobState.waiting || (JC.state === JobState.stuck && stuckTrigger < now.toMillis)
|
||||
|
||||
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"WHERE" ++ and(uworker.is(worker), stateCond) ++
|
||||
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" ++
|
||||
fr"WHERE" ++ stateCond
|
||||
val sql1 =
|
||||
Select(
|
||||
max(JC.group).as("g"),
|
||||
from(JC).innerJoin(G, JC.group === G.group),
|
||||
G.worker === worker && stateCond
|
||||
)
|
||||
|
||||
val union =
|
||||
sql"SELECT g FROM ((" ++ sql1 ++ sql") UNION ALL (" ++ sql2 ++ sql")) as t0 WHERE g is not null"
|
||||
val sql2 =
|
||||
Select(min(JC.group).as("g"), from(JC), stateCond)
|
||||
|
||||
union
|
||||
.query[Ident]
|
||||
.to[List]
|
||||
.map(
|
||||
_.headOption
|
||||
) // either one or two results, but may be empty if RJob table is empty
|
||||
val gcol = Column[String]("g", TableDef(""))
|
||||
val groups =
|
||||
Select(select(gcol), from(union(sql1, sql2), "t0"), gcol.isNull.negate)
|
||||
|
||||
// either 0, one or two results, but may be empty if RJob table is empty
|
||||
groups.build.query[Ident].to[List].map(_.headOption)
|
||||
}
|
||||
|
||||
private def stuckTriggerValue(t: RJob.Table, initialPause: Duration, now: Timestamp) =
|
||||
plus(
|
||||
coalesce(t.startedmillis.s, lit(now.toMillis)).s,
|
||||
mult(power(2, t.retries.s).s, lit(initialPause.millis)).s
|
||||
)
|
||||
|
||||
def selectNextJob(
|
||||
group: Ident,
|
||||
prio: Priority,
|
||||
initialPause: Duration,
|
||||
now: Timestamp
|
||||
): ConnectionIO[Option[RJob]] = {
|
||||
val JC = RJob.Columns
|
||||
val JC = RJob.T
|
||||
val psort =
|
||||
if (prio == Priority.High) JC.priority.desc
|
||||
else JC.priority.asc
|
||||
val waiting: JobState = JobState.Waiting
|
||||
val stuck: JobState = JobState.Stuck
|
||||
val waiting = JobState.waiting
|
||||
val stuck = JobState.stuck
|
||||
|
||||
val stuckTrigger =
|
||||
coalesce(JC.startedmillis.f, sql"${now.toMillis}") ++ fr"+" ++ power2(
|
||||
JC.retries
|
||||
) ++ 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) ++
|
||||
fr"LIMIT 1"
|
||||
val stuckTrigger = stuckTriggerValue(JC, initialPause, now)
|
||||
val sql =
|
||||
Select(
|
||||
select(JC.all),
|
||||
from(JC),
|
||||
JC.group === group && (JC.state === waiting ||
|
||||
(JC.state === stuck && stuckTrigger < now.toMillis))
|
||||
).orderBy(JC.state.asc, psort, JC.submitted.asc).limit(1)
|
||||
|
||||
sql.query[RJob].option
|
||||
sql.build.query[RJob].option
|
||||
}
|
||||
|
||||
def setCancelled[F[_]: Effect](id: Ident, store: Store[F]): F[Unit] =
|
||||
@ -212,39 +204,34 @@ object QJob {
|
||||
collective: Ident,
|
||||
max: Long
|
||||
): Stream[ConnectionIO, (RJob, Vector[RJobLog])] = {
|
||||
val JC = RJob.Columns
|
||||
val waiting: Set[JobState] = Set(JobState.Waiting, JobState.Stuck, JobState.Scheduled)
|
||||
val running: Set[JobState] = Set(JobState.Running)
|
||||
val done = JobState.all.diff(waiting).diff(running)
|
||||
val JC = RJob.T
|
||||
val waiting = NonEmptyList.of(JobState.Waiting, JobState.Stuck, JobState.Scheduled)
|
||||
val running = NonEmptyList.of(JobState.Running)
|
||||
//val done = JobState.all.filterNot(js => ).diff(waiting).diff(running)
|
||||
|
||||
def selectJobs(now: Timestamp): Stream[ConnectionIO, RJob] = {
|
||||
val refDate = now.minusHours(24)
|
||||
val runningJobs = Select(
|
||||
select(JC.all),
|
||||
from(JC),
|
||||
JC.group === collective && JC.state.in(running)
|
||||
).orderBy(JC.submitted.desc).build.query[RJob].stream
|
||||
|
||||
val runningJobs = (selectSimple(
|
||||
JC.all,
|
||||
RJob.table,
|
||||
and(JC.group.is(collective), JC.state.isOneOf(running.toSeq))
|
||||
) ++ orderBy(JC.submitted.desc)).query[RJob].stream
|
||||
val waitingJobs = Select(
|
||||
select(JC.all),
|
||||
from(JC),
|
||||
JC.group === collective && JC.state.in(waiting) && JC.submitted > refDate
|
||||
).orderBy(JC.submitted.desc).build.query[RJob].stream.take(max)
|
||||
|
||||
val waitingJobs = (selectSimple(
|
||||
JC.all,
|
||||
RJob.table,
|
||||
val doneJobs = Select(
|
||||
select(JC.all),
|
||||
from(JC),
|
||||
and(
|
||||
JC.group.is(collective),
|
||||
JC.state.isOneOf(waiting.toSeq),
|
||||
JC.submitted.isGt(refDate)
|
||||
JC.group === collective,
|
||||
JC.state.in(JobState.done),
|
||||
JC.submitted > refDate
|
||||
)
|
||||
) ++ orderBy(JC.submitted.desc)).query[RJob].stream.take(max)
|
||||
|
||||
val doneJobs = (selectSimple(
|
||||
JC.all,
|
||||
RJob.table,
|
||||
and(
|
||||
JC.group.is(collective),
|
||||
JC.state.isOneOf(done.toSeq),
|
||||
JC.submitted.isGt(refDate)
|
||||
)
|
||||
) ++ orderBy(JC.submitted.desc)).query[RJob].stream.take(max)
|
||||
).orderBy(JC.submitted.desc).build.query[RJob].stream.take(max)
|
||||
|
||||
runningJobs ++ waitingJobs ++ doneJobs
|
||||
}
|
||||
|
@ -3,9 +3,8 @@ package docspell.store.queries
|
||||
import cats.data.OptionT
|
||||
|
||||
import docspell.common._
|
||||
import docspell.store.impl.Implicits._
|
||||
import docspell.store.records.RCollective.{Columns => CC}
|
||||
import docspell.store.records.RUser.{Columns => UC}
|
||||
import docspell.store.qb.DSL._
|
||||
import docspell.store.qb._
|
||||
import docspell.store.records.{RCollective, RRememberMe, RUser}
|
||||
|
||||
import doobie._
|
||||
@ -23,19 +22,14 @@ object QLogin {
|
||||
)
|
||||
|
||||
def findUser(acc: AccountId): ConnectionIO[Option[Data]] = {
|
||||
val ucid = UC.cid.prefix("u")
|
||||
val login = UC.login.prefix("u")
|
||||
val pass = UC.password.prefix("u")
|
||||
val ustate = UC.state.prefix("u")
|
||||
val cstate = CC.state.prefix("c")
|
||||
val ccid = CC.id.prefix("c")
|
||||
|
||||
val sql = selectSimple(
|
||||
List(ucid, login, pass, cstate, ustate),
|
||||
RUser.table ++ fr"u, " ++ RCollective.table ++ fr"c",
|
||||
and(ucid.is(ccid), login.is(acc.user), ucid.is(acc.collective))
|
||||
)
|
||||
|
||||
val user = RUser.as("u")
|
||||
val coll = RCollective.as("c")
|
||||
val sql =
|
||||
Select(
|
||||
select(user.cid, user.login, user.password, coll.state, user.state),
|
||||
from(user).innerJoin(coll, user.cid === coll.id),
|
||||
user.login === acc.user && user.cid === acc.collective
|
||||
).build
|
||||
logger.trace(s"SQL : $sql")
|
||||
sql.query[Data].option
|
||||
}
|
||||
|
@ -3,8 +3,8 @@ package docspell.store.queries
|
||||
import cats.data.OptionT
|
||||
|
||||
import docspell.common._
|
||||
import docspell.store.impl.Column
|
||||
import docspell.store.impl.Implicits._
|
||||
import docspell.store.qb.DSL._
|
||||
import docspell.store.qb._
|
||||
import docspell.store.records._
|
||||
|
||||
import doobie._
|
||||
@ -12,6 +12,11 @@ import doobie.implicits._
|
||||
|
||||
object QMails {
|
||||
|
||||
private val item = RItem.as("i")
|
||||
private val smail = RSentMail.as("sm")
|
||||
private val mailitem = RSentMailItem.as("mi")
|
||||
private val user = RUser.as("u")
|
||||
|
||||
def delete(coll: Ident, mailId: Ident): ConnectionIO[Int] =
|
||||
(for {
|
||||
m <- OptionT(findMail(coll, mailId))
|
||||
@ -19,47 +24,28 @@ object QMails {
|
||||
n <- OptionT.liftF(RSentMail.delete(m._1.id))
|
||||
} yield k + n).getOrElse(0)
|
||||
|
||||
def findMail(coll: Ident, mailId: Ident): ConnectionIO[Option[(RSentMail, Ident)]] = {
|
||||
val iColl = RItem.Columns.cid.prefix("i")
|
||||
val mId = RSentMail.Columns.id.prefix("m")
|
||||
def findMail(coll: Ident, mailId: Ident): ConnectionIO[Option[(RSentMail, Ident)]] =
|
||||
partialFind
|
||||
.where(smail.id === mailId && item.cid === coll)
|
||||
.build
|
||||
.query[(RSentMail, Ident)]
|
||||
.option
|
||||
|
||||
val (cols, from) = partialFind
|
||||
|
||||
val cond = Seq(mId.is(mailId), iColl.is(coll))
|
||||
|
||||
selectSimple(cols, from, and(cond)).query[(RSentMail, Ident)].option
|
||||
}
|
||||
|
||||
def findMails(coll: Ident, itemId: Ident): ConnectionIO[Vector[(RSentMail, Ident)]] = {
|
||||
val iColl = RItem.Columns.cid.prefix("i")
|
||||
val tItem = RSentMailItem.Columns.itemId.prefix("t")
|
||||
val mCreated = RSentMail.Columns.created.prefix("m")
|
||||
|
||||
val (cols, from) = partialFind
|
||||
|
||||
val cond = Seq(tItem.is(itemId), iColl.is(coll))
|
||||
|
||||
(selectSimple(cols, from, and(cond)) ++ orderBy(mCreated.f) ++ fr"DESC")
|
||||
def findMails(coll: Ident, itemId: Ident): ConnectionIO[Vector[(RSentMail, Ident)]] =
|
||||
partialFind
|
||||
.where(mailitem.itemId === itemId && item.cid === coll)
|
||||
.orderBy(smail.created.desc)
|
||||
.build
|
||||
.query[(RSentMail, Ident)]
|
||||
.to[Vector]
|
||||
}
|
||||
|
||||
private def partialFind: (Seq[Column], Fragment) = {
|
||||
val iId = RItem.Columns.id.prefix("i")
|
||||
val tItem = RSentMailItem.Columns.itemId.prefix("t")
|
||||
val tMail = RSentMailItem.Columns.sentMailId.prefix("t")
|
||||
val mId = RSentMail.Columns.id.prefix("m")
|
||||
val mUser = RSentMail.Columns.uid.prefix("m")
|
||||
val uId = RUser.Columns.uid.prefix("u")
|
||||
val uLogin = RUser.Columns.login.prefix("u")
|
||||
|
||||
val cols = RSentMail.Columns.all.map(_.prefix("m")) :+ uLogin
|
||||
val from = RSentMail.table ++ fr"m INNER JOIN" ++
|
||||
RSentMailItem.table ++ fr"t ON" ++ tMail.is(mId) ++
|
||||
fr"INNER JOIN" ++ RItem.table ++ fr"i ON" ++ tItem.is(iId) ++
|
||||
fr"INNER JOIN" ++ RUser.table ++ fr"u ON" ++ uId.is(mUser)
|
||||
|
||||
(cols, from)
|
||||
}
|
||||
private def partialFind: Select.SimpleSelect =
|
||||
Select(
|
||||
select(smail.all).append(user.login.s),
|
||||
from(smail)
|
||||
.innerJoin(mailitem, mailitem.sentMailId === smail.id)
|
||||
.innerJoin(item, mailitem.itemId === item.id)
|
||||
.innerJoin(user, user.uid === smail.uid)
|
||||
)
|
||||
|
||||
}
|
||||
|
@ -0,0 +1,51 @@
|
||||
package docspell.store.queries
|
||||
|
||||
import cats.data.OptionT
|
||||
import cats.effect._
|
||||
import cats.implicits._
|
||||
|
||||
import docspell.common._
|
||||
import docspell.store.records._
|
||||
|
||||
import doobie.implicits._
|
||||
import doobie.{Query => _, _}
|
||||
|
||||
object QMoveAttachment {
|
||||
def moveAttachmentBefore(
|
||||
itemId: Ident,
|
||||
source: Ident,
|
||||
target: Ident
|
||||
): ConnectionIO[Int] = {
|
||||
|
||||
// rs < rt
|
||||
def moveBack(rs: RAttachment, rt: RAttachment): ConnectionIO[Int] =
|
||||
for {
|
||||
n <- RAttachment.decPositions(itemId, rs.position, rt.position)
|
||||
k <- RAttachment.updatePosition(rs.id, rt.position)
|
||||
} yield n + k
|
||||
|
||||
// rs > rt
|
||||
def moveForward(rs: RAttachment, rt: RAttachment): ConnectionIO[Int] =
|
||||
for {
|
||||
n <- RAttachment.incPositions(itemId, rt.position, rs.position)
|
||||
k <- RAttachment.updatePosition(rs.id, rt.position)
|
||||
} yield n + k
|
||||
|
||||
(for {
|
||||
_ <- OptionT.liftF(
|
||||
if (source == target)
|
||||
Sync[ConnectionIO].raiseError(new Exception("Attachments are the same!"))
|
||||
else ().pure[ConnectionIO]
|
||||
)
|
||||
rs <- OptionT(RAttachment.findById(source)).filter(_.itemId == itemId)
|
||||
rt <- OptionT(RAttachment.findById(target)).filter(_.itemId == itemId)
|
||||
n <- OptionT.liftF(
|
||||
if (rs.position == rt.position || rs.position + 1 == rt.position)
|
||||
0.pure[ConnectionIO]
|
||||
else if (rs.position < rt.position) moveBack(rs, rt)
|
||||
else moveForward(rs, rt)
|
||||
)
|
||||
} yield n).getOrElse(0)
|
||||
|
||||
}
|
||||
}
|
@ -4,10 +4,8 @@ import cats.implicits._
|
||||
import fs2._
|
||||
|
||||
import docspell.common._
|
||||
import docspell.store.impl.Column
|
||||
import docspell.store.impl.Implicits._
|
||||
import docspell.store.records.ROrganization.{Columns => OC}
|
||||
import docspell.store.records.RPerson.{Columns => PC}
|
||||
import docspell.store.qb.DSL._
|
||||
import docspell.store.qb._
|
||||
import docspell.store.records._
|
||||
import docspell.store.{AddResult, Store}
|
||||
|
||||
@ -15,33 +13,26 @@ import doobie._
|
||||
import doobie.implicits._
|
||||
|
||||
object QOrganization {
|
||||
private val p = RPerson.as("p")
|
||||
private val c = RContact.as("c")
|
||||
private val org = ROrganization.as("o")
|
||||
|
||||
def findOrgAndContact(
|
||||
coll: Ident,
|
||||
query: Option[String],
|
||||
order: OC.type => Column
|
||||
order: ROrganization.Table => Column[_]
|
||||
): Stream[ConnectionIO, (ROrganization, Vector[RContact])] = {
|
||||
val oColl = ROrganization.Columns.cid.prefix("o")
|
||||
val oName = ROrganization.Columns.name.prefix("o")
|
||||
val oNotes = ROrganization.Columns.notes.prefix("o")
|
||||
val oId = ROrganization.Columns.oid.prefix("o")
|
||||
val cOrg = RContact.Columns.orgId.prefix("c")
|
||||
val cVal = RContact.Columns.value.prefix("c")
|
||||
val valFilter = query.map { q =>
|
||||
val v = s"%$q%"
|
||||
c.value.like(v) || org.name.like(v) || org.notes.like(v)
|
||||
}
|
||||
val sql = Select(
|
||||
select(org.all, c.all),
|
||||
from(org).leftJoin(c, c.orgId === org.oid),
|
||||
org.cid === coll &&? valFilter
|
||||
).orderBy(order(org))
|
||||
|
||||
val cols = ROrganization.Columns.all.map(_.prefix("o")) ++ RContact.Columns.all
|
||||
.map(_.prefix("c"))
|
||||
val from = ROrganization.table ++ fr"o LEFT JOIN" ++
|
||||
RContact.table ++ fr"c ON" ++ cOrg.is(oId)
|
||||
|
||||
val q = Seq(oColl.is(coll)) ++ (query match {
|
||||
case Some(str) =>
|
||||
val v = s"%$str%"
|
||||
Seq(or(cVal.lowerLike(v), oName.lowerLike(v), oNotes.lowerLike(v)))
|
||||
case None =>
|
||||
Seq.empty
|
||||
})
|
||||
|
||||
(selectSimple(cols, from, and(q)) ++ orderBy(order(OC).prefix("o").f))
|
||||
sql.build
|
||||
.query[(ROrganization, Option[RContact])]
|
||||
.stream
|
||||
.groupAdjacentBy(_._1)
|
||||
@ -55,18 +46,13 @@ object QOrganization {
|
||||
coll: Ident,
|
||||
orgId: Ident
|
||||
): ConnectionIO[Option[(ROrganization, Vector[RContact])]] = {
|
||||
val oColl = ROrganization.Columns.cid.prefix("o")
|
||||
val oId = ROrganization.Columns.oid.prefix("o")
|
||||
val cOrg = RContact.Columns.orgId.prefix("c")
|
||||
val sql = run(
|
||||
select(org.all, c.all),
|
||||
from(org).leftJoin(c, c.orgId === org.oid),
|
||||
org.cid === coll && org.oid === orgId
|
||||
)
|
||||
|
||||
val cols = ROrganization.Columns.all.map(_.prefix("o")) ++ RContact.Columns.all
|
||||
.map(_.prefix("c"))
|
||||
val from = ROrganization.table ++ fr"o LEFT JOIN" ++
|
||||
RContact.table ++ fr"c ON" ++ cOrg.is(oId)
|
||||
|
||||
val q = and(oColl.is(coll), oId.is(orgId))
|
||||
|
||||
selectSimple(cols, from, q)
|
||||
sql
|
||||
.query[(ROrganization, Option[RContact])]
|
||||
.stream
|
||||
.groupAdjacentBy(_._1)
|
||||
@ -81,33 +67,20 @@ object QOrganization {
|
||||
def findPersonAndContact(
|
||||
coll: Ident,
|
||||
query: Option[String],
|
||||
order: PC.type => Column
|
||||
order: RPerson.Table => Column[_]
|
||||
): Stream[ConnectionIO, (RPerson, Option[ROrganization], Vector[RContact])] = {
|
||||
val pColl = PC.cid.prefix("p")
|
||||
val pName = RPerson.Columns.name.prefix("p")
|
||||
val pNotes = RPerson.Columns.notes.prefix("p")
|
||||
val pId = RPerson.Columns.pid.prefix("p")
|
||||
val cPers = RContact.Columns.personId.prefix("c")
|
||||
val cVal = RContact.Columns.value.prefix("c")
|
||||
val oId = ROrganization.Columns.oid.prefix("o")
|
||||
val pOid = RPerson.Columns.oid.prefix("p")
|
||||
val valFilter = query
|
||||
.map(s => s"%$s%")
|
||||
.map(v => c.value.like(v) || p.name.like(v) || p.notes.like(v))
|
||||
val sql = Select(
|
||||
select(p.all, org.all, c.all),
|
||||
from(p)
|
||||
.leftJoin(org, org.oid === p.oid)
|
||||
.leftJoin(c, c.personId === p.pid),
|
||||
p.cid === coll &&? valFilter
|
||||
).orderBy(order(p))
|
||||
|
||||
val cols = RPerson.Columns.all.map(_.prefix("p")) ++
|
||||
ROrganization.Columns.all.map(_.prefix("o")) ++
|
||||
RContact.Columns.all.map(_.prefix("c"))
|
||||
val from = RPerson.table ++ fr"p LEFT JOIN" ++
|
||||
ROrganization.table ++ fr"o ON" ++ pOid.is(oId) ++ fr"LEFT JOIN" ++
|
||||
RContact.table ++ fr"c ON" ++ cPers.is(pId)
|
||||
|
||||
val q = Seq(pColl.is(coll)) ++ (query match {
|
||||
case Some(str) =>
|
||||
val v = s"%${str.toLowerCase}%"
|
||||
Seq(or(cVal.lowerLike(v), pName.lowerLike(v), pNotes.lowerLike(v)))
|
||||
case None =>
|
||||
Seq.empty
|
||||
})
|
||||
|
||||
(selectSimple(cols, from, and(q)) ++ orderBy(order(PC).prefix("p").f))
|
||||
sql.build
|
||||
.query[(RPerson, Option[ROrganization], Option[RContact])]
|
||||
.stream
|
||||
.groupAdjacentBy(_._1)
|
||||
@ -122,22 +95,16 @@ object QOrganization {
|
||||
coll: Ident,
|
||||
persId: Ident
|
||||
): ConnectionIO[Option[(RPerson, Option[ROrganization], Vector[RContact])]] = {
|
||||
val pColl = PC.cid.prefix("p")
|
||||
val pId = RPerson.Columns.pid.prefix("p")
|
||||
val cPers = RContact.Columns.personId.prefix("c")
|
||||
val oId = ROrganization.Columns.oid.prefix("o")
|
||||
val pOid = RPerson.Columns.oid.prefix("p")
|
||||
val sql =
|
||||
run(
|
||||
select(p.all, org.all, c.all),
|
||||
from(p)
|
||||
.leftJoin(org, p.oid === org.oid)
|
||||
.leftJoin(c, c.personId === p.pid),
|
||||
p.cid === coll && p.pid === persId
|
||||
)
|
||||
|
||||
val cols = RPerson.Columns.all.map(_.prefix("p")) ++
|
||||
ROrganization.Columns.all.map(_.prefix("o")) ++
|
||||
RContact.Columns.all.map(_.prefix("c"))
|
||||
val from = RPerson.table ++ fr"p LEFT JOIN" ++
|
||||
ROrganization.table ++ fr"o ON" ++ pOid.is(oId) ++ fr"LEFT JOIN" ++
|
||||
RContact.table ++ fr"c ON" ++ cPers.is(pId)
|
||||
|
||||
val q = and(pColl.is(coll), pId.is(persId))
|
||||
|
||||
selectSimple(cols, from, q)
|
||||
sql
|
||||
.query[(RPerson, Option[ROrganization], Option[RContact])]
|
||||
.stream
|
||||
.groupAdjacentBy(_._1)
|
||||
@ -155,25 +122,14 @@ object QOrganization {
|
||||
value: String,
|
||||
ck: Option[ContactKind],
|
||||
concerning: Option[Boolean]
|
||||
): Stream[ConnectionIO, RPerson] = {
|
||||
val pColl = PC.cid.prefix("p")
|
||||
val pConc = PC.concerning.prefix("p")
|
||||
val pId = PC.pid.prefix("p")
|
||||
val cPers = RContact.Columns.personId.prefix("c")
|
||||
val cVal = RContact.Columns.value.prefix("c")
|
||||
val cKind = RContact.Columns.kind.prefix("c")
|
||||
|
||||
val from = RPerson.table ++ fr"p INNER JOIN" ++
|
||||
RContact.table ++ fr"c ON" ++ cPers.is(pId)
|
||||
val q = Seq(
|
||||
cVal.lowerLike(s"%${value.toLowerCase}%"),
|
||||
pColl.is(coll)
|
||||
) ++ concerning.map(pConc.is(_)).toSeq ++ ck.map(cKind.is(_)).toSeq
|
||||
|
||||
selectDistinct(PC.all.map(_.prefix("p")), from, and(q))
|
||||
.query[RPerson]
|
||||
.stream
|
||||
}
|
||||
): Stream[ConnectionIO, RPerson] =
|
||||
runDistinct(
|
||||
select(p.all),
|
||||
from(p).innerJoin(c, c.personId === p.pid),
|
||||
c.value.like(s"%${value.toLowerCase}%") && p.cid === coll &&?
|
||||
concerning.map(c => p.concerning === c) &&?
|
||||
ck.map(k => c.kind === k)
|
||||
).query[RPerson].stream
|
||||
|
||||
def addOrg[F[_]](
|
||||
org: ROrganization,
|
||||
|
@ -1,7 +1,8 @@
|
||||
package docspell.store.queries
|
||||
|
||||
import docspell.common._
|
||||
import docspell.store.impl.Implicits._
|
||||
import docspell.store.qb.DSL._
|
||||
import docspell.store.qb._
|
||||
import docspell.store.records._
|
||||
|
||||
import doobie._
|
||||
@ -9,47 +10,47 @@ import doobie.implicits._
|
||||
|
||||
object QPeriodicTask {
|
||||
|
||||
def clearWorkers(name: Ident): ConnectionIO[Int] = {
|
||||
val worker = RPeriodicTask.Columns.worker
|
||||
updateRow(RPeriodicTask.table, worker.is(name), worker.setTo[Ident](None)).update.run
|
||||
}
|
||||
private val RT = RPeriodicTask.T
|
||||
|
||||
def setWorker(pid: Ident, name: Ident, ts: Timestamp): ConnectionIO[Int] = {
|
||||
val id = RPeriodicTask.Columns.id
|
||||
val worker = RPeriodicTask.Columns.worker
|
||||
val marked = RPeriodicTask.Columns.marked
|
||||
updateRow(
|
||||
RPeriodicTask.table,
|
||||
and(id.is(pid), worker.isNull),
|
||||
commas(worker.setTo(name), marked.setTo(ts))
|
||||
).update.run
|
||||
}
|
||||
def clearWorkers(name: Ident): ConnectionIO[Int] =
|
||||
DML.update(
|
||||
RT,
|
||||
RT.worker === name,
|
||||
DML.set(RT.worker.setTo(None: Option[Ident]))
|
||||
)
|
||||
|
||||
def setWorker(pid: Ident, name: Ident, ts: Timestamp): ConnectionIO[Int] =
|
||||
DML
|
||||
.update(
|
||||
RT,
|
||||
RT.id === pid && RT.worker.isNull,
|
||||
DML.set(
|
||||
RT.worker.setTo(name),
|
||||
RT.marked.setTo(ts)
|
||||
)
|
||||
)
|
||||
|
||||
def unsetWorker(
|
||||
pid: Ident,
|
||||
nextRun: Option[Timestamp]
|
||||
): ConnectionIO[Int] = {
|
||||
val id = RPeriodicTask.Columns.id
|
||||
val worker = RPeriodicTask.Columns.worker
|
||||
val next = RPeriodicTask.Columns.nextrun
|
||||
updateRow(
|
||||
RPeriodicTask.table,
|
||||
id.is(pid),
|
||||
commas(worker.setTo[Ident](None), next.setTo(nextRun))
|
||||
).update.run
|
||||
}
|
||||
): ConnectionIO[Int] =
|
||||
DML.update(
|
||||
RT,
|
||||
RT.id === pid,
|
||||
DML.set(
|
||||
RT.worker.setTo(None),
|
||||
RT.nextrun.setTo(nextRun)
|
||||
)
|
||||
)
|
||||
|
||||
def findNext(excl: Option[Ident]): ConnectionIO[Option[RPeriodicTask]] = {
|
||||
val enabled = RPeriodicTask.Columns.enabled
|
||||
val pid = RPeriodicTask.Columns.id
|
||||
val order = orderBy(RPeriodicTask.Columns.nextrun.f) ++ fr"ASC"
|
||||
|
||||
val where = excl match {
|
||||
case Some(id) => and(pid.isNot(id), enabled.is(true))
|
||||
case None => enabled.is(true)
|
||||
case Some(id) => RT.id <> id && RT.enabled === true
|
||||
case None => RT.enabled === true
|
||||
}
|
||||
val sql =
|
||||
selectSimple(RPeriodicTask.Columns.all, RPeriodicTask.table, where) ++ order
|
||||
Select(select(RT.all), from(RT), where).orderBy(RT.nextrun.asc).build
|
||||
|
||||
sql.query[RPeriodicTask].streamWithChunkSize(2).take(1).compile.last
|
||||
}
|
||||
}
|
||||
|
@ -3,33 +3,34 @@ package docspell.store.queries
|
||||
import fs2._
|
||||
|
||||
import docspell.common._
|
||||
import docspell.store.impl.Implicits._
|
||||
import docspell.store.qb.DSL._
|
||||
import docspell.store.qb._
|
||||
import docspell.store.records._
|
||||
import docspell.store.usertask.UserTask
|
||||
|
||||
import doobie._
|
||||
|
||||
object QUserTask {
|
||||
private val cols = RPeriodicTask.Columns
|
||||
private val RT = RPeriodicTask.T
|
||||
|
||||
def findAll(account: AccountId): Stream[ConnectionIO, UserTask[String]] =
|
||||
selectSimple(
|
||||
RPeriodicTask.Columns.all,
|
||||
RPeriodicTask.table,
|
||||
and(cols.group.is(account.collective), cols.submitter.is(account.user))
|
||||
run(
|
||||
select(RT.all),
|
||||
from(RT),
|
||||
RT.group === account.collective && RT.submitter === account.user
|
||||
).query[RPeriodicTask].stream.map(makeUserTask)
|
||||
|
||||
def findByName(
|
||||
account: AccountId,
|
||||
name: Ident
|
||||
): Stream[ConnectionIO, UserTask[String]] =
|
||||
selectSimple(
|
||||
RPeriodicTask.Columns.all,
|
||||
RPeriodicTask.table,
|
||||
and(
|
||||
cols.group.is(account.collective),
|
||||
cols.submitter.is(account.user),
|
||||
cols.task.is(name)
|
||||
run(
|
||||
select(RT.all),
|
||||
from(RT),
|
||||
where(
|
||||
RT.group === account.collective,
|
||||
RT.submitter === account.user,
|
||||
RT.task === name
|
||||
)
|
||||
).query[RPeriodicTask].stream.map(makeUserTask)
|
||||
|
||||
@ -37,13 +38,13 @@ object QUserTask {
|
||||
account: AccountId,
|
||||
id: Ident
|
||||
): ConnectionIO[Option[UserTask[String]]] =
|
||||
selectSimple(
|
||||
RPeriodicTask.Columns.all,
|
||||
RPeriodicTask.table,
|
||||
and(
|
||||
cols.group.is(account.collective),
|
||||
cols.submitter.is(account.user),
|
||||
cols.id.is(id)
|
||||
run(
|
||||
select(RT.all),
|
||||
from(RT),
|
||||
where(
|
||||
RT.group === account.collective,
|
||||
RT.submitter === account.user,
|
||||
RT.id === id
|
||||
)
|
||||
).query[RPeriodicTask].option.map(_.map(makeUserTask))
|
||||
|
||||
@ -63,24 +64,25 @@ object QUserTask {
|
||||
RPeriodicTask.exists(id)
|
||||
|
||||
def delete(account: AccountId, id: Ident): ConnectionIO[Int] =
|
||||
deleteFrom(
|
||||
RPeriodicTask.table,
|
||||
and(
|
||||
cols.group.is(account.collective),
|
||||
cols.submitter.is(account.user),
|
||||
cols.id.is(id)
|
||||
DML
|
||||
.delete(
|
||||
RT,
|
||||
where(
|
||||
RT.group === account.collective,
|
||||
RT.submitter === account.user,
|
||||
RT.id === id
|
||||
)
|
||||
)
|
||||
).update.run
|
||||
|
||||
def deleteAll(account: AccountId, name: Ident): ConnectionIO[Int] =
|
||||
deleteFrom(
|
||||
RPeriodicTask.table,
|
||||
and(
|
||||
cols.group.is(account.collective),
|
||||
cols.submitter.is(account.user),
|
||||
cols.task.is(name)
|
||||
DML.delete(
|
||||
RT,
|
||||
where(
|
||||
RT.group === account.collective,
|
||||
RT.submitter === account.user,
|
||||
RT.task === name
|
||||
)
|
||||
)
|
||||
).update.run
|
||||
|
||||
def makeUserTask(r: RPeriodicTask): UserTask[String] =
|
||||
UserTask(r.id, r.task, r.enabled, r.timer, r.args)
|
||||
|
@ -0,0 +1,57 @@
|
||||
package docspell.store.queries
|
||||
|
||||
import docspell.common._
|
||||
import docspell.store.records.RItem
|
||||
|
||||
case class Query(
|
||||
account: AccountId,
|
||||
name: Option[String],
|
||||
states: Seq[ItemState],
|
||||
direction: Option[Direction],
|
||||
corrPerson: Option[Ident],
|
||||
corrOrg: Option[Ident],
|
||||
concPerson: Option[Ident],
|
||||
concEquip: Option[Ident],
|
||||
folder: Option[Ident],
|
||||
tagsInclude: List[Ident],
|
||||
tagsExclude: List[Ident],
|
||||
tagCategoryIncl: List[String],
|
||||
tagCategoryExcl: List[String],
|
||||
dateFrom: Option[Timestamp],
|
||||
dateTo: Option[Timestamp],
|
||||
dueDateFrom: Option[Timestamp],
|
||||
dueDateTo: Option[Timestamp],
|
||||
allNames: Option[String],
|
||||
itemIds: Option[Set[Ident]],
|
||||
customValues: Seq[CustomValue],
|
||||
source: Option[String],
|
||||
orderAsc: Option[RItem.Table => docspell.store.qb.Column[_]]
|
||||
)
|
||||
|
||||
object Query {
|
||||
def empty(account: AccountId): Query =
|
||||
Query(
|
||||
account,
|
||||
None,
|
||||
Seq.empty,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
Nil,
|
||||
Nil,
|
||||
Nil,
|
||||
Nil,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
Seq.empty,
|
||||
None,
|
||||
None
|
||||
)
|
||||
}
|
@ -2,6 +2,9 @@ package docspell.store.queries
|
||||
|
||||
object QueryWildcard {
|
||||
|
||||
def lower(s: String): String =
|
||||
apply(s.toLowerCase)
|
||||
|
||||
def apply(value: String): String = {
|
||||
def prefix(n: String) =
|
||||
if (n.startsWith("*")) s"%${n.substring(1)}"
|
||||
|
@ -0,0 +1,8 @@
|
||||
package docspell.store.queries
|
||||
|
||||
case class SearchSummary(
|
||||
count: Int,
|
||||
tags: List[TagCount],
|
||||
fields: List[FieldStats],
|
||||
folders: List[FolderCount]
|
||||
)
|
@ -0,0 +1,6 @@
|
||||
package docspell.store.queries
|
||||
|
||||
import docspell.common._
|
||||
|
||||
/** Some preselected item from a fulltext search. */
|
||||
case class SelectedItem(itemId: Ident, weight: Double)
|
@ -0,0 +1,5 @@
|
||||
package docspell.store.queries
|
||||
|
||||
import docspell.store.records.RTag
|
||||
|
||||
case class TagCount(tag: RTag, count: Int)
|
@ -0,0 +1,9 @@
|
||||
package docspell.store.queries
|
||||
|
||||
import docspell.common._
|
||||
|
||||
case class TextAndTag(itemId: Ident, text: String, tag: Option[TextAndTag.TagName])
|
||||
|
||||
object TextAndTag {
|
||||
case class TagName(id: Ident, name: String)
|
||||
}
|
@ -5,8 +5,8 @@ import cats.implicits._
|
||||
import fs2.Stream
|
||||
|
||||
import docspell.common._
|
||||
import docspell.store.impl.Implicits._
|
||||
import docspell.store.impl._
|
||||
import docspell.store.qb.DSL._
|
||||
import docspell.store.qb._
|
||||
|
||||
import bitpeace.FileMeta
|
||||
import doobie._
|
||||
@ -22,44 +22,52 @@ case class RAttachment(
|
||||
) {}
|
||||
|
||||
object RAttachment {
|
||||
final case class Table(alias: Option[String]) extends TableDef {
|
||||
val tableName = "attachment"
|
||||
|
||||
val table = fr"attachment"
|
||||
|
||||
object Columns {
|
||||
val id = Column("attachid")
|
||||
val itemId = Column("itemid")
|
||||
val fileId = Column("filemetaid")
|
||||
val position = Column("position")
|
||||
val created = Column("created")
|
||||
val name = Column("name")
|
||||
val all = List(id, itemId, fileId, position, created, name)
|
||||
val id = Column[Ident]("attachid", this)
|
||||
val itemId = Column[Ident]("itemid", this)
|
||||
val fileId = Column[Ident]("filemetaid", this)
|
||||
val position = Column[Int]("position", this)
|
||||
val created = Column[Timestamp]("created", this)
|
||||
val name = Column[String]("name", this)
|
||||
val all = NonEmptyList.of[Column[_]](id, itemId, fileId, position, created, name)
|
||||
}
|
||||
import Columns._
|
||||
|
||||
val T = Table(None)
|
||||
def as(alias: String): Table =
|
||||
Table(Some(alias))
|
||||
|
||||
def insert(v: RAttachment): ConnectionIO[Int] =
|
||||
insertRow(
|
||||
table,
|
||||
all,
|
||||
DML.insert(
|
||||
T,
|
||||
T.all,
|
||||
fr"${v.id},${v.itemId},${v.fileId.id},${v.position},${v.created},${v.name}"
|
||||
).update.run
|
||||
)
|
||||
|
||||
def decPositions(iId: Ident, lowerBound: Int, upperBound: Int): ConnectionIO[Int] =
|
||||
updateRow(
|
||||
table,
|
||||
and(itemId.is(iId), position.isGte(lowerBound), position.isLte(upperBound)),
|
||||
position.decrement(1)
|
||||
).update.run
|
||||
DML.update(
|
||||
T,
|
||||
where(
|
||||
T.itemId === iId && T.position >= lowerBound && T.position <= upperBound
|
||||
),
|
||||
DML.set(T.position.decrement(1))
|
||||
)
|
||||
|
||||
def incPositions(iId: Ident, lowerBound: Int, upperBound: Int): ConnectionIO[Int] =
|
||||
updateRow(
|
||||
table,
|
||||
and(itemId.is(iId), position.isGte(lowerBound), position.isLte(upperBound)),
|
||||
position.increment(1)
|
||||
).update.run
|
||||
DML.update(
|
||||
T,
|
||||
where(
|
||||
T.itemId === iId && T.position >= lowerBound && T.position <= upperBound
|
||||
),
|
||||
DML.set(T.position.increment(1))
|
||||
)
|
||||
|
||||
def nextPosition(id: Ident): ConnectionIO[Int] =
|
||||
for {
|
||||
max <- selectSimple(position.max, table, itemId.is(id)).query[Option[Int]].unique
|
||||
max <- Select(max(T.position).s, from(T), T.itemId === id).build
|
||||
.query[Option[Int]]
|
||||
.unique
|
||||
} yield max.map(_ + 1).getOrElse(0)
|
||||
|
||||
def updateFileIdAndName(
|
||||
@ -67,41 +75,39 @@ object RAttachment {
|
||||
fId: Ident,
|
||||
fname: Option[String]
|
||||
): ConnectionIO[Int] =
|
||||
updateRow(
|
||||
table,
|
||||
id.is(attachId),
|
||||
commas(fileId.setTo(fId), name.setTo(fname))
|
||||
).update.run
|
||||
DML.update(
|
||||
T,
|
||||
T.id === attachId,
|
||||
DML.set(T.fileId.setTo(fId), T.name.setTo(fname))
|
||||
)
|
||||
|
||||
def updateFileId(
|
||||
attachId: Ident,
|
||||
fId: Ident
|
||||
): ConnectionIO[Int] =
|
||||
updateRow(
|
||||
table,
|
||||
id.is(attachId),
|
||||
fileId.setTo(fId)
|
||||
).update.run
|
||||
DML.update(
|
||||
T,
|
||||
T.id === attachId,
|
||||
DML.set(T.fileId.setTo(fId))
|
||||
)
|
||||
|
||||
def updatePosition(attachId: Ident, pos: Int): ConnectionIO[Int] =
|
||||
updateRow(table, id.is(attachId), position.setTo(pos)).update.run
|
||||
DML.update(T, T.id === attachId, DML.set(T.position.setTo(pos)))
|
||||
|
||||
def findById(attachId: Ident): ConnectionIO[Option[RAttachment]] =
|
||||
selectSimple(all, table, id.is(attachId)).query[RAttachment].option
|
||||
run(select(T.all), from(T), T.id === attachId).query[RAttachment].option
|
||||
|
||||
def findMeta(attachId: Ident): ConnectionIO[Option[FileMeta]] = {
|
||||
import bitpeace.sql._
|
||||
|
||||
val cols = RFileMeta.Columns.all.map(_.prefix("m"))
|
||||
val aId = id.prefix("a")
|
||||
val aFileMeta = fileId.prefix("a")
|
||||
val mId = RFileMeta.Columns.id.prefix("m")
|
||||
|
||||
val from =
|
||||
table ++ fr"a INNER JOIN" ++ RFileMeta.table ++ fr"m ON" ++ aFileMeta.is(mId)
|
||||
val cond = aId.is(attachId)
|
||||
|
||||
selectSimple(cols, from, cond).query[FileMeta].option
|
||||
val m = RFileMeta.as("m")
|
||||
val a = RAttachment.as("a")
|
||||
Select(
|
||||
select(m.all),
|
||||
from(a)
|
||||
.innerJoin(m, a.fileId === m.id),
|
||||
a.id === attachId
|
||||
).build.query[FileMeta].option
|
||||
}
|
||||
|
||||
def updateName(
|
||||
@ -109,7 +115,7 @@ object RAttachment {
|
||||
collective: Ident,
|
||||
aname: Option[String]
|
||||
): ConnectionIO[Int] = {
|
||||
val update = updateRow(table, id.is(attachId), name.setTo(aname)).update.run
|
||||
val update = DML.update(T, T.id === attachId, DML.set(T.name.setTo(aname)))
|
||||
for {
|
||||
exists <- existsByIdAndCollective(attachId, collective)
|
||||
n <- if (exists) update else 0.pure[ConnectionIO]
|
||||
@ -119,44 +125,45 @@ object RAttachment {
|
||||
def findByIdAndCollective(
|
||||
attachId: Ident,
|
||||
collective: Ident
|
||||
): ConnectionIO[Option[RAttachment]] =
|
||||
selectSimple(
|
||||
all.map(_.prefix("a")),
|
||||
table ++ fr"a," ++ RItem.table ++ fr"i",
|
||||
and(
|
||||
fr"a.itemid = i.itemid",
|
||||
id.prefix("a").is(attachId),
|
||||
RItem.Columns.cid.prefix("i").is(collective)
|
||||
)
|
||||
).query[RAttachment].option
|
||||
): ConnectionIO[Option[RAttachment]] = {
|
||||
val a = RAttachment.as("a")
|
||||
val i = RItem.as("i")
|
||||
Select(
|
||||
select(a.all),
|
||||
from(a).innerJoin(i, a.itemId === i.id),
|
||||
a.id === attachId && i.cid === collective
|
||||
).build.query[RAttachment].option
|
||||
}
|
||||
|
||||
def findByItem(id: Ident): ConnectionIO[Vector[RAttachment]] =
|
||||
selectSimple(all, table, itemId.is(id)).query[RAttachment].to[Vector]
|
||||
run(select(T.all), from(T), T.itemId === id).query[RAttachment].to[Vector]
|
||||
|
||||
def existsByIdAndCollective(
|
||||
attachId: Ident,
|
||||
collective: Ident
|
||||
): ConnectionIO[Boolean] = {
|
||||
val aId = id.prefix("a")
|
||||
val aItem = itemId.prefix("a")
|
||||
val iId = RItem.Columns.id.prefix("i")
|
||||
val iColl = RItem.Columns.cid.prefix("i")
|
||||
val from =
|
||||
table ++ fr"a INNER JOIN" ++ RItem.table ++ fr"i ON" ++ aItem.is(iId)
|
||||
val cond = and(iColl.is(collective), aId.is(attachId))
|
||||
selectCount(id, from, cond).query[Int].unique.map(_ > 0)
|
||||
val a = RAttachment.as("a")
|
||||
val i = RItem.as("i")
|
||||
Select(
|
||||
count(a.id).s,
|
||||
from(a)
|
||||
.innerJoin(i, a.itemId === i.id),
|
||||
i.cid === collective && a.id === attachId
|
||||
).build.query[Int].unique.map(_ > 0)
|
||||
}
|
||||
|
||||
def findByItemAndCollective(
|
||||
id: Ident,
|
||||
coll: Ident
|
||||
): ConnectionIO[Vector[RAttachment]] = {
|
||||
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"WHERE" ++ and(itemId.prefix("a").is(id), RItem.Columns.cid.prefix("i").is(coll))
|
||||
q.query[RAttachment].to[Vector]
|
||||
val a = RAttachment.as("a")
|
||||
val i = RItem.as("i")
|
||||
Select(
|
||||
select(a.all),
|
||||
from(a)
|
||||
.innerJoin(i, i.id === a.itemId),
|
||||
a.itemId === id && i.cid === coll
|
||||
).build.query[RAttachment].to[Vector]
|
||||
}
|
||||
|
||||
def findByItemCollectiveSource(
|
||||
@ -164,29 +171,20 @@ object RAttachment {
|
||||
coll: Ident,
|
||||
fileIds: NonEmptyList[Ident]
|
||||
): ConnectionIO[Vector[RAttachment]] = {
|
||||
val i = RItem.as("i")
|
||||
val a = RAttachment.as("a")
|
||||
val s = RAttachmentSource.as("s")
|
||||
val r = RAttachmentArchive.as("r")
|
||||
|
||||
val iId = RItem.Columns.id.prefix("i")
|
||||
val iColl = RItem.Columns.cid.prefix("i")
|
||||
val aItem = Columns.itemId.prefix("a")
|
||||
val aId = Columns.id.prefix("a")
|
||||
val aFile = Columns.fileId.prefix("a")
|
||||
val sId = RAttachmentSource.Columns.id.prefix("s")
|
||||
val sFile = RAttachmentSource.Columns.fileId.prefix("s")
|
||||
val rId = RAttachmentArchive.Columns.id.prefix("r")
|
||||
val rFile = RAttachmentArchive.Columns.fileId.prefix("r")
|
||||
|
||||
val from = table ++ fr"a INNER JOIN" ++
|
||||
RItem.table ++ fr"i ON" ++ iId.is(aItem) ++ fr"LEFT JOIN" ++
|
||||
RAttachmentSource.table ++ fr"s ON" ++ sId.is(aId) ++ fr"LEFT JOIN" ++
|
||||
RAttachmentArchive.table ++ fr"r ON" ++ rId.is(aId)
|
||||
|
||||
val cond = and(
|
||||
iId.is(id),
|
||||
iColl.is(coll),
|
||||
or(aFile.isIn(fileIds), sFile.isIn(fileIds), rFile.isIn(fileIds))
|
||||
)
|
||||
|
||||
selectSimple(all.map(_.prefix("a")), from, cond).query[RAttachment].to[Vector]
|
||||
Select(
|
||||
select(a.all),
|
||||
from(a)
|
||||
.innerJoin(i, i.id === a.itemId)
|
||||
.leftJoin(s, s.id === a.id)
|
||||
.leftJoin(r, r.id === a.id),
|
||||
i.id === id && i.cid === coll &&
|
||||
(a.fileId.in(fileIds) || s.fileId.in(fileIds) || r.fileId.in(fileIds))
|
||||
).build.query[RAttachment].to[Vector]
|
||||
}
|
||||
|
||||
def findByItemAndCollectiveWithMeta(
|
||||
@ -195,27 +193,29 @@ object RAttachment {
|
||||
): ConnectionIO[Vector[(RAttachment, FileMeta)]] = {
|
||||
import bitpeace.sql._
|
||||
|
||||
val cols = all.map(_.prefix("a")) ++ RFileMeta.Columns.all.map(_.prefix("m"))
|
||||
val afileMeta = fileId.prefix("a")
|
||||
val aItem = itemId.prefix("a")
|
||||
val mId = RFileMeta.Columns.id.prefix("m")
|
||||
val iId = RItem.Columns.id.prefix("i")
|
||||
val iColl = RItem.Columns.cid.prefix("i")
|
||||
|
||||
val from =
|
||||
table ++ fr"a INNER JOIN" ++ RFileMeta.table ++ fr"m ON" ++ afileMeta.is(mId) ++
|
||||
fr"INNER JOIN" ++ RItem.table ++ fr"i ON" ++ aItem.is(iId)
|
||||
val cond = Seq(aItem.is(id), iColl.is(coll))
|
||||
|
||||
selectSimple(cols, from, and(cond)).query[(RAttachment, FileMeta)].to[Vector]
|
||||
val a = RAttachment.as("a")
|
||||
val m = RFileMeta.as("m")
|
||||
val i = RItem.as("i")
|
||||
Select(
|
||||
select(a.all, m.all),
|
||||
from(a)
|
||||
.innerJoin(m, a.fileId === m.id)
|
||||
.innerJoin(i, a.itemId === i.id),
|
||||
a.itemId === id && i.cid === coll
|
||||
).build.query[(RAttachment, FileMeta)].to[Vector]
|
||||
}
|
||||
|
||||
def findByItemWithMeta(id: Ident): ConnectionIO[Vector[(RAttachment, FileMeta)]] = {
|
||||
import bitpeace.sql._
|
||||
|
||||
val q =
|
||||
fr"SELECT a.*,m.* FROM" ++ table ++ fr"a, filemeta m WHERE a.filemetaid = m.id AND a.itemid = $id ORDER BY a.position ASC"
|
||||
q.query[(RAttachment, FileMeta)].to[Vector]
|
||||
val a = RAttachment.as("a")
|
||||
val m = RFileMeta.as("m")
|
||||
Select(
|
||||
select(a.all, m.all),
|
||||
from(a)
|
||||
.innerJoin(m, a.fileId === m.id),
|
||||
a.itemId === id
|
||||
).orderBy(a.position.asc).build.query[(RAttachment, FileMeta)].to[Vector]
|
||||
}
|
||||
|
||||
/** Deletes the attachment and its related source and meta records.
|
||||
@ -225,110 +225,80 @@ object RAttachment {
|
||||
n0 <- RAttachmentMeta.delete(attachId)
|
||||
n1 <- RAttachmentSource.delete(attachId)
|
||||
n2 <- RAttachmentPreview.delete(attachId)
|
||||
n3 <- deleteFrom(table, id.is(attachId)).update.run
|
||||
n3 <- DML.delete(T, T.id === attachId)
|
||||
} yield n0 + n1 + n2 + n3
|
||||
|
||||
def findItemId(attachId: Ident): ConnectionIO[Option[Ident]] =
|
||||
selectSimple(Seq(itemId), table, id.is(attachId)).query[Ident].option
|
||||
Select(T.itemId.s, from(T), T.id === attachId).build.query[Ident].option
|
||||
|
||||
def findAll(
|
||||
coll: Option[Ident],
|
||||
chunkSize: Int
|
||||
): Stream[ConnectionIO, RAttachment] = {
|
||||
val aItem = Columns.itemId.prefix("a")
|
||||
val iId = RItem.Columns.id.prefix("i")
|
||||
val iColl = RItem.Columns.cid.prefix("i")
|
||||
|
||||
val cols = all.map(_.prefix("a"))
|
||||
val a = RAttachment.as("a")
|
||||
val i = RItem.as("i")
|
||||
|
||||
coll match {
|
||||
case Some(cid) =>
|
||||
val join = table ++ fr"a INNER JOIN" ++ RItem.table ++ fr"i ON" ++ iId.is(aItem)
|
||||
val cond = iColl.is(cid)
|
||||
selectSimple(cols, join, cond)
|
||||
.query[RAttachment]
|
||||
.streamWithChunkSize(chunkSize)
|
||||
Select(
|
||||
select(a.all),
|
||||
from(a)
|
||||
.innerJoin(i, i.id === a.itemId),
|
||||
i.cid === cid
|
||||
).build.query[RAttachment].streamWithChunkSize(chunkSize)
|
||||
case None =>
|
||||
selectSimple(cols, table, Fragment.empty)
|
||||
Select(select(a.all), from(a)).build
|
||||
.query[RAttachment]
|
||||
.streamWithChunkSize(chunkSize)
|
||||
}
|
||||
}
|
||||
|
||||
def findAllWithoutPageCount(chunkSize: Int): Stream[ConnectionIO, RAttachment] = {
|
||||
val aId = Columns.id.prefix("a")
|
||||
val aCreated = Columns.created.prefix("a")
|
||||
val mId = RAttachmentMeta.Columns.id.prefix("m")
|
||||
val mPages = RAttachmentMeta.Columns.pages.prefix("m")
|
||||
|
||||
val cols = all.map(_.prefix("a"))
|
||||
val join = table ++ fr"a LEFT OUTER JOIN" ++
|
||||
RAttachmentMeta.table ++ fr"m ON" ++ aId.is(mId)
|
||||
val cond = mPages.isNull
|
||||
|
||||
(selectSimple(cols, join, cond) ++ orderBy(aCreated.desc))
|
||||
.query[RAttachment]
|
||||
.streamWithChunkSize(chunkSize)
|
||||
val a = RAttachment.as("a")
|
||||
val m = RAttachmentMeta.as("m")
|
||||
Select(
|
||||
select(a.all),
|
||||
from(a)
|
||||
.leftJoin(m, a.id === m.id),
|
||||
m.pages.isNull
|
||||
).build.query[RAttachment].streamWithChunkSize(chunkSize)
|
||||
}
|
||||
|
||||
def findWithoutPreview(
|
||||
coll: Option[Ident],
|
||||
chunkSize: Int
|
||||
): Stream[ConnectionIO, RAttachment] = {
|
||||
val aId = Columns.id.prefix("a")
|
||||
val aItem = Columns.itemId.prefix("a")
|
||||
val aCreated = Columns.created.prefix("a")
|
||||
val pId = RAttachmentPreview.Columns.id.prefix("p")
|
||||
val iId = RItem.Columns.id.prefix("i")
|
||||
val iColl = RItem.Columns.cid.prefix("i")
|
||||
val a = RAttachment.as("a")
|
||||
val p = RAttachmentPreview.as("p")
|
||||
val i = RItem.as("i")
|
||||
|
||||
val cols = all.map(_.prefix("a"))
|
||||
val baseJoin =
|
||||
table ++ fr"a LEFT OUTER JOIN" ++
|
||||
RAttachmentPreview.table ++ fr"p ON" ++ pId.is(aId)
|
||||
|
||||
val baseCond =
|
||||
Seq(pId.isNull)
|
||||
|
||||
coll match {
|
||||
case Some(cid) =>
|
||||
val join = baseJoin ++ fr"INNER JOIN" ++ RItem.table ++ fr"i ON" ++ iId.is(aItem)
|
||||
val cond = and(baseCond ++ Seq(iColl.is(cid)))
|
||||
(selectSimple(cols, join, cond) ++ orderBy(aCreated.desc))
|
||||
.query[RAttachment]
|
||||
.streamWithChunkSize(chunkSize)
|
||||
case None =>
|
||||
(selectSimple(cols, baseJoin, and(baseCond)) ++ orderBy(aCreated.desc))
|
||||
.query[RAttachment]
|
||||
.streamWithChunkSize(chunkSize)
|
||||
}
|
||||
val baseJoin = from(a).leftJoin(p, p.id === a.id)
|
||||
Select(
|
||||
select(a.all),
|
||||
coll.map(_ => baseJoin.innerJoin(i, i.id === a.itemId)).getOrElse(baseJoin),
|
||||
p.id.isNull &&? coll.map(cid => i.cid === cid)
|
||||
).orderBy(a.created.asc).build.query[RAttachment].streamWithChunkSize(chunkSize)
|
||||
}
|
||||
|
||||
def findNonConvertedPdf(
|
||||
coll: Option[Ident],
|
||||
chunkSize: Int
|
||||
): Stream[ConnectionIO, RAttachment] = {
|
||||
val aId = Columns.id.prefix("a")
|
||||
val aItem = Columns.itemId.prefix("a")
|
||||
val aFile = Columns.fileId.prefix("a")
|
||||
val sId = RAttachmentSource.Columns.id.prefix("s")
|
||||
val sFile = RAttachmentSource.Columns.fileId.prefix("s")
|
||||
val iId = RItem.Columns.id.prefix("i")
|
||||
val iColl = RItem.Columns.cid.prefix("i")
|
||||
val mId = RFileMeta.Columns.id.prefix("m")
|
||||
val mType = RFileMeta.Columns.mimetype.prefix("m")
|
||||
val pdfType = "application/pdf%"
|
||||
val a = RAttachment.as("a")
|
||||
val s = RAttachmentSource.as("s")
|
||||
val i = RItem.as("i")
|
||||
val m = RFileMeta.as("m")
|
||||
|
||||
val from = table ++ fr"a INNER JOIN" ++
|
||||
RAttachmentSource.table ++ fr"s ON" ++ sId.is(aId) ++ fr"INNER JOIN" ++
|
||||
RItem.table ++ fr"i ON" ++ iId.is(aItem) ++ fr"INNER JOIN" ++
|
||||
RFileMeta.table ++ fr"m ON" ++ aFile.is(mId)
|
||||
val where = coll match {
|
||||
case Some(cid) => and(iColl.is(cid), aFile.is(sFile), mType.lowerLike(pdfType))
|
||||
case None => and(aFile.is(sFile), mType.lowerLike(pdfType))
|
||||
}
|
||||
selectSimple(all.map(_.prefix("a")), from, where)
|
||||
.query[RAttachment]
|
||||
.streamWithChunkSize(chunkSize)
|
||||
Select(
|
||||
select(a.all),
|
||||
from(a)
|
||||
.innerJoin(s, s.id === a.id)
|
||||
.innerJoin(i, i.id === a.itemId)
|
||||
.innerJoin(m, m.id === a.fileId),
|
||||
a.fileId === s.fileId &&
|
||||
m.mimetype.likes(pdfType) &&?
|
||||
coll.map(cid => i.cid === cid)
|
||||
).build.query[RAttachment].streamWithChunkSize(chunkSize)
|
||||
}
|
||||
}
|
||||
|
@ -3,8 +3,9 @@ package docspell.store.records
|
||||
import cats.data.NonEmptyList
|
||||
|
||||
import docspell.common._
|
||||
import docspell.store.impl.Implicits._
|
||||
import docspell.store.impl._
|
||||
import docspell.store.qb.DSL._
|
||||
import docspell.store.qb.TableDef
|
||||
import docspell.store.qb._
|
||||
|
||||
import bitpeace.FileMeta
|
||||
import doobie._
|
||||
@ -22,77 +23,71 @@ case class RAttachmentArchive(
|
||||
)
|
||||
|
||||
object RAttachmentArchive {
|
||||
final case class Table(alias: Option[String]) extends TableDef {
|
||||
val tableName = "attachment_archive"
|
||||
|
||||
val table = fr"attachment_archive"
|
||||
val id = Column[Ident]("id", this)
|
||||
val fileId = Column[Ident]("file_id", this)
|
||||
val name = Column[String]("filename", this)
|
||||
val messageId = Column[String]("message_id", this)
|
||||
val created = Column[Timestamp]("created", this)
|
||||
|
||||
object Columns {
|
||||
val id = Column("id")
|
||||
val fileId = Column("file_id")
|
||||
val name = Column("filename")
|
||||
val messageId = Column("message_id")
|
||||
val created = Column("created")
|
||||
|
||||
val all = List(id, fileId, name, messageId, created)
|
||||
val all = NonEmptyList.of[Column[_]](id, fileId, name, messageId, created)
|
||||
}
|
||||
|
||||
import Columns._
|
||||
val T = Table(None)
|
||||
def as(alias: String): Table =
|
||||
Table(Some(alias))
|
||||
|
||||
def of(ra: RAttachment, mId: Option[String]): RAttachmentArchive =
|
||||
RAttachmentArchive(ra.id, ra.fileId, ra.name, mId, ra.created)
|
||||
|
||||
def insert(v: RAttachmentArchive): ConnectionIO[Int] =
|
||||
insertRow(
|
||||
table,
|
||||
all,
|
||||
DML.insert(
|
||||
T,
|
||||
T.all,
|
||||
fr"${v.id},${v.fileId},${v.name},${v.messageId},${v.created}"
|
||||
).update.run
|
||||
)
|
||||
|
||||
def findById(attachId: Ident): ConnectionIO[Option[RAttachmentArchive]] =
|
||||
selectSimple(all, table, id.is(attachId)).query[RAttachmentArchive].option
|
||||
run(select(T.all), from(T), T.id === attachId).query[RAttachmentArchive].option
|
||||
|
||||
def delete(attachId: Ident): ConnectionIO[Int] =
|
||||
deleteFrom(table, id.is(attachId)).update.run
|
||||
DML.delete(T, T.id === attachId)
|
||||
|
||||
def deleteAll(fId: Ident): ConnectionIO[Int] =
|
||||
deleteFrom(table, fileId.is(fId)).update.run
|
||||
DML.delete(T, T.fileId === fId)
|
||||
|
||||
def findByIdAndCollective(
|
||||
attachId: Ident,
|
||||
collective: Ident
|
||||
): ConnectionIO[Option[RAttachmentArchive]] = {
|
||||
val bId = RAttachment.Columns.id.prefix("b")
|
||||
val aId = Columns.id.prefix("a")
|
||||
val bItem = RAttachment.Columns.itemId.prefix("b")
|
||||
val iId = RItem.Columns.id.prefix("i")
|
||||
val iColl = RItem.Columns.cid.prefix("i")
|
||||
val b = RAttachment.as("b")
|
||||
val a = RAttachmentArchive.as("a")
|
||||
val i = RItem.as("i")
|
||||
|
||||
val from = table ++ fr"a INNER JOIN" ++
|
||||
RAttachment.table ++ fr"b ON" ++ aId.is(bId) ++
|
||||
fr"INNER JOIN" ++ RItem.table ++ fr"i ON" ++ bItem.is(iId)
|
||||
|
||||
val where = and(aId.is(attachId), bId.is(attachId), iColl.is(collective))
|
||||
|
||||
selectSimple(all.map(_.prefix("a")), from, where).query[RAttachmentArchive].option
|
||||
Select(
|
||||
select(a.all),
|
||||
from(a)
|
||||
.innerJoin(b, b.id === a.id)
|
||||
.innerJoin(i, i.id === b.itemId),
|
||||
a.id === attachId && b.id === attachId && i.cid === collective
|
||||
).build.query[RAttachmentArchive].option
|
||||
}
|
||||
|
||||
def findByMessageIdAndCollective(
|
||||
messageIds: NonEmptyList[String],
|
||||
collective: Ident
|
||||
): ConnectionIO[Vector[RAttachmentArchive]] = {
|
||||
val bId = RAttachment.Columns.id.prefix("b")
|
||||
val bItem = RAttachment.Columns.itemId.prefix("b")
|
||||
val aMsgId = Columns.messageId.prefix("a")
|
||||
val aId = Columns.id.prefix("a")
|
||||
val iId = RItem.Columns.id.prefix("i")
|
||||
val iColl = RItem.Columns.cid.prefix("i")
|
||||
|
||||
val from = table ++ fr"a INNER JOIN" ++
|
||||
RAttachment.table ++ fr"b ON" ++ aId.is(bId) ++
|
||||
fr"INNER JOIN" ++ RItem.table ++ fr"i ON" ++ bItem.is(iId)
|
||||
|
||||
val where = and(aMsgId.isIn(messageIds), iColl.is(collective))
|
||||
|
||||
selectSimple(all.map(_.prefix("a")), from, where).query[RAttachmentArchive].to[Vector]
|
||||
val b = RAttachment.as("b")
|
||||
val a = RAttachmentArchive.as("a")
|
||||
val i = RItem.as("i")
|
||||
Select(
|
||||
select(a.all),
|
||||
from(a)
|
||||
.innerJoin(b, b.id === a.id)
|
||||
.innerJoin(i, i.id === b.itemId),
|
||||
a.messageId.in(messageIds) && i.cid === collective
|
||||
).build.query[RAttachmentArchive].to[Vector]
|
||||
}
|
||||
|
||||
def findByItemWithMeta(
|
||||
@ -100,31 +95,27 @@ object RAttachmentArchive {
|
||||
): ConnectionIO[Vector[(RAttachmentArchive, FileMeta)]] = {
|
||||
import bitpeace.sql._
|
||||
|
||||
val aId = Columns.id.prefix("a")
|
||||
val afileMeta = fileId.prefix("a")
|
||||
val bPos = RAttachment.Columns.position.prefix("b")
|
||||
val bId = RAttachment.Columns.id.prefix("b")
|
||||
val bItem = RAttachment.Columns.itemId.prefix("b")
|
||||
val mId = RFileMeta.Columns.id.prefix("m")
|
||||
|
||||
val cols = all.map(_.prefix("a")) ++ RFileMeta.Columns.all.map(_.prefix("m"))
|
||||
val from = table ++ fr"a INNER JOIN" ++
|
||||
RFileMeta.table ++ fr"m ON" ++ afileMeta.is(mId) ++ fr"INNER JOIN" ++
|
||||
RAttachment.table ++ fr"b ON" ++ aId.is(bId)
|
||||
val where = bItem.is(id)
|
||||
|
||||
(selectSimple(cols, from, where) ++ orderBy(bPos.asc))
|
||||
.query[(RAttachmentArchive, FileMeta)]
|
||||
.to[Vector]
|
||||
val a = RAttachmentArchive.as("a")
|
||||
val b = RAttachment.as("b")
|
||||
val m = RFileMeta.as("m")
|
||||
Select(
|
||||
select(a.all, m.all),
|
||||
from(a)
|
||||
.innerJoin(m, a.fileId === m.id)
|
||||
.innerJoin(b, a.id === b.id),
|
||||
b.itemId === id
|
||||
).orderBy(b.position.asc).build.query[(RAttachmentArchive, FileMeta)].to[Vector]
|
||||
}
|
||||
|
||||
/** If the given attachment id has an associated archive, this returns
|
||||
* the number of all associated attachments. Returns 0 if there is
|
||||
* no archive for the given attachment.
|
||||
*/
|
||||
def countEntries(attachId: Ident): ConnectionIO[Int] = {
|
||||
val qFileId = selectSimple(Seq(fileId), table, id.is(attachId))
|
||||
val q = selectCount(id, table, fileId.isSubquery(qFileId))
|
||||
q.query[Int].unique
|
||||
}
|
||||
def countEntries(attachId: Ident): ConnectionIO[Int] =
|
||||
Select(
|
||||
count(T.id).s,
|
||||
from(T),
|
||||
T.fileId.in(Select(T.fileId.s, from(T), T.id === attachId))
|
||||
).build.query[Int].unique
|
||||
|
||||
}
|
||||
|
@ -1,10 +1,11 @@
|
||||
package docspell.store.records
|
||||
|
||||
import cats.data.NonEmptyList
|
||||
import cats.implicits._
|
||||
|
||||
import docspell.common._
|
||||
import docspell.store.impl.Implicits._
|
||||
import docspell.store.impl._
|
||||
import docspell.store.qb.DSL._
|
||||
import docspell.store.qb._
|
||||
|
||||
import doobie._
|
||||
import doobie.implicits._
|
||||
@ -29,33 +30,36 @@ object RAttachmentMeta {
|
||||
def empty(attachId: Ident) =
|
||||
RAttachmentMeta(attachId, None, Nil, MetaProposalList.empty, None)
|
||||
|
||||
val table = fr"attachmentmeta"
|
||||
final case class Table(alias: Option[String]) extends TableDef {
|
||||
val tableName = "attachmentmeta"
|
||||
|
||||
object Columns {
|
||||
val id = Column("attachid")
|
||||
val content = Column("content")
|
||||
val nerlabels = Column("nerlabels")
|
||||
val proposals = Column("itemproposals")
|
||||
val pages = Column("page_count")
|
||||
val all = List(id, content, nerlabels, proposals, pages)
|
||||
val id = Column[Ident]("attachid", this)
|
||||
val content = Column[String]("content", this)
|
||||
val nerlabels = Column[List[NerLabel]]("nerlabels", this)
|
||||
val proposals = Column[MetaProposalList]("itemproposals", this)
|
||||
val pages = Column[Int]("page_count", this)
|
||||
val all = NonEmptyList.of[Column[_]](id, content, nerlabels, proposals, pages)
|
||||
}
|
||||
import Columns._
|
||||
|
||||
val T = Table(None)
|
||||
def as(alias: String): Table =
|
||||
Table(Some(alias))
|
||||
|
||||
def insert(v: RAttachmentMeta): ConnectionIO[Int] =
|
||||
insertRow(
|
||||
table,
|
||||
all,
|
||||
DML.insert(
|
||||
T,
|
||||
T.all,
|
||||
fr"${v.id},${v.content},${v.nerlabels},${v.proposals},${v.pages}"
|
||||
).update.run
|
||||
)
|
||||
|
||||
def exists(attachId: Ident): ConnectionIO[Boolean] =
|
||||
selectCount(id, table, id.is(attachId)).query[Int].unique.map(_ > 0)
|
||||
Select(count(T.id).s, from(T), T.id === attachId).build.query[Int].unique.map(_ > 0)
|
||||
|
||||
def findById(attachId: Ident): ConnectionIO[Option[RAttachmentMeta]] =
|
||||
selectSimple(all, table, id.is(attachId)).query[RAttachmentMeta].option
|
||||
run(select(T.all), from(T), T.id === attachId).query[RAttachmentMeta].option
|
||||
|
||||
def findPageCountById(attachId: Ident): ConnectionIO[Option[Int]] =
|
||||
selectSimple(Seq(pages), table, id.is(attachId))
|
||||
Select(T.pages.s, from(T), T.id === attachId).build
|
||||
.query[Option[Int]]
|
||||
.option
|
||||
.map(_.flatten)
|
||||
@ -67,37 +71,37 @@ object RAttachmentMeta {
|
||||
} yield n1
|
||||
|
||||
def update(v: RAttachmentMeta): ConnectionIO[Int] =
|
||||
updateRow(
|
||||
table,
|
||||
id.is(v.id),
|
||||
commas(
|
||||
content.setTo(v.content),
|
||||
nerlabels.setTo(v.nerlabels),
|
||||
proposals.setTo(v.proposals)
|
||||
DML.update(
|
||||
T,
|
||||
T.id === v.id,
|
||||
DML.set(
|
||||
T.content.setTo(v.content),
|
||||
T.nerlabels.setTo(v.nerlabels),
|
||||
T.proposals.setTo(v.proposals)
|
||||
)
|
||||
)
|
||||
).update.run
|
||||
|
||||
def updateLabels(mid: Ident, labels: List[NerLabel]): ConnectionIO[Int] =
|
||||
updateRow(
|
||||
table,
|
||||
id.is(mid),
|
||||
commas(
|
||||
nerlabels.setTo(labels)
|
||||
DML.update(
|
||||
T,
|
||||
T.id === mid,
|
||||
DML.set(
|
||||
T.nerlabels.setTo(labels)
|
||||
)
|
||||
)
|
||||
).update.run
|
||||
|
||||
def updateProposals(mid: Ident, plist: MetaProposalList): ConnectionIO[Int] =
|
||||
updateRow(
|
||||
table,
|
||||
id.is(mid),
|
||||
commas(
|
||||
proposals.setTo(plist)
|
||||
DML.update(
|
||||
T,
|
||||
T.id === mid,
|
||||
DML.set(
|
||||
T.proposals.setTo(plist)
|
||||
)
|
||||
)
|
||||
).update.run
|
||||
|
||||
def updatePageCount(mid: Ident, pageCount: Option[Int]): ConnectionIO[Int] =
|
||||
updateRow(table, id.is(mid), pages.setTo(pageCount)).update.run
|
||||
DML.update(T, T.id === mid, DML.set(T.pages.setTo(pageCount)))
|
||||
|
||||
def delete(attachId: Ident): ConnectionIO[Int] =
|
||||
deleteFrom(table, id.is(attachId)).update.run
|
||||
DML.delete(T, T.id === attachId)
|
||||
}
|
||||
|
@ -1,8 +1,10 @@
|
||||
package docspell.store.records
|
||||
|
||||
import cats.data.NonEmptyList
|
||||
|
||||
import docspell.common._
|
||||
import docspell.store.impl.Implicits._
|
||||
import docspell.store.impl._
|
||||
import docspell.store.qb.DSL._
|
||||
import docspell.store.qb._
|
||||
|
||||
import bitpeace.FileMeta
|
||||
import doobie._
|
||||
@ -19,79 +21,73 @@ case class RAttachmentPreview(
|
||||
)
|
||||
|
||||
object RAttachmentPreview {
|
||||
final case class Table(alias: Option[String]) extends TableDef {
|
||||
val tableName = "attachment_preview"
|
||||
|
||||
val table = fr"attachment_preview"
|
||||
val id = Column[Ident]("id", this)
|
||||
val fileId = Column[Ident]("file_id", this)
|
||||
val name = Column[String]("filename", this)
|
||||
val created = Column[Timestamp]("created", this)
|
||||
|
||||
object Columns {
|
||||
val id = Column("id")
|
||||
val fileId = Column("file_id")
|
||||
val name = Column("filename")
|
||||
val created = Column("created")
|
||||
|
||||
val all = List(id, fileId, name, created)
|
||||
val all = NonEmptyList.of[Column[_]](id, fileId, name, created)
|
||||
}
|
||||
|
||||
import Columns._
|
||||
val T = Table(None)
|
||||
def as(alias: String): Table =
|
||||
Table(Some(alias))
|
||||
|
||||
def insert(v: RAttachmentPreview): ConnectionIO[Int] =
|
||||
insertRow(table, all, fr"${v.id},${v.fileId},${v.name},${v.created}").update.run
|
||||
DML.insert(T, T.all, fr"${v.id},${v.fileId},${v.name},${v.created}")
|
||||
|
||||
def findById(attachId: Ident): ConnectionIO[Option[RAttachmentPreview]] =
|
||||
selectSimple(all, table, id.is(attachId)).query[RAttachmentPreview].option
|
||||
run(select(T.all), from(T), T.id === attachId).query[RAttachmentPreview].option
|
||||
|
||||
def delete(attachId: Ident): ConnectionIO[Int] =
|
||||
deleteFrom(table, id.is(attachId)).update.run
|
||||
DML.delete(T, T.id === attachId)
|
||||
|
||||
def findByIdAndCollective(
|
||||
attachId: Ident,
|
||||
collective: Ident
|
||||
): ConnectionIO[Option[RAttachmentPreview]] = {
|
||||
val bId = RAttachment.Columns.id.prefix("b")
|
||||
val aId = Columns.id.prefix("a")
|
||||
val bItem = RAttachment.Columns.itemId.prefix("b")
|
||||
val iId = RItem.Columns.id.prefix("i")
|
||||
val iColl = RItem.Columns.cid.prefix("i")
|
||||
val b = RAttachment.as("b")
|
||||
val a = RAttachmentPreview.as("a")
|
||||
val i = RItem.as("i")
|
||||
|
||||
val from = table ++ fr"a INNER JOIN" ++
|
||||
RAttachment.table ++ fr"b ON" ++ aId.is(bId) ++
|
||||
fr"INNER JOIN" ++ RItem.table ++ fr"i ON" ++ bItem.is(iId)
|
||||
|
||||
val where = and(aId.is(attachId), bId.is(attachId), iColl.is(collective))
|
||||
|
||||
selectSimple(all.map(_.prefix("a")), from, where).query[RAttachmentPreview].option
|
||||
Select(
|
||||
select(a.all),
|
||||
from(a)
|
||||
.innerJoin(b, a.id === b.id)
|
||||
.innerJoin(i, i.id === b.itemId),
|
||||
a.id === attachId && b.id === attachId && i.cid === collective
|
||||
).build.query[RAttachmentPreview].option
|
||||
}
|
||||
|
||||
def findByItem(itemId: Ident): ConnectionIO[Vector[RAttachmentPreview]] = {
|
||||
val sId = Columns.id.prefix("s")
|
||||
val aId = RAttachment.Columns.id.prefix("a")
|
||||
val aItem = RAttachment.Columns.itemId.prefix("a")
|
||||
|
||||
val from = table ++ fr"s INNER JOIN" ++ RAttachment.table ++ fr"a ON" ++ sId.is(aId)
|
||||
selectSimple(all.map(_.prefix("s")), from, aItem.is(itemId))
|
||||
.query[RAttachmentPreview]
|
||||
.to[Vector]
|
||||
val s = RAttachmentPreview.as("s")
|
||||
val a = RAttachment.as("a")
|
||||
Select(
|
||||
select(s.all),
|
||||
from(s)
|
||||
.innerJoin(a, s.id === a.id),
|
||||
a.itemId === itemId
|
||||
).build.query[RAttachmentPreview].to[Vector]
|
||||
}
|
||||
|
||||
def findByItemAndCollective(
|
||||
itemId: Ident,
|
||||
coll: Ident
|
||||
): ConnectionIO[Option[RAttachmentPreview]] = {
|
||||
val sId = Columns.id.prefix("s")
|
||||
val aId = RAttachment.Columns.id.prefix("a")
|
||||
val aItem = RAttachment.Columns.itemId.prefix("a")
|
||||
val aPos = RAttachment.Columns.position.prefix("a")
|
||||
val iId = RItem.Columns.id.prefix("i")
|
||||
val iColl = RItem.Columns.cid.prefix("i")
|
||||
val s = RAttachmentPreview.as("s")
|
||||
val a = RAttachment.as("a")
|
||||
val i = RItem.as("i")
|
||||
|
||||
val from =
|
||||
table ++ fr"s INNER JOIN" ++ RAttachment.table ++ fr"a ON" ++ sId.is(aId) ++
|
||||
fr"INNER JOIN" ++ RItem.table ++ fr"i ON" ++ iId.is(aItem)
|
||||
|
||||
selectSimple(
|
||||
all.map(_.prefix("s")) ++ List(aPos),
|
||||
from,
|
||||
and(aItem.is(itemId), iColl.is(coll))
|
||||
)
|
||||
Select(
|
||||
select(s.all).append(a.position.s),
|
||||
from(s)
|
||||
.innerJoin(a, s.id === a.id)
|
||||
.innerJoin(i, i.id === a.itemId),
|
||||
a.itemId === itemId && i.cid === coll
|
||||
).build
|
||||
.query[(RAttachmentPreview, Int)]
|
||||
.to[Vector]
|
||||
.map(_.sortBy(_._2).headOption.map(_._1))
|
||||
@ -102,22 +98,16 @@ object RAttachmentPreview {
|
||||
): ConnectionIO[Vector[(RAttachmentPreview, FileMeta)]] = {
|
||||
import bitpeace.sql._
|
||||
|
||||
val aId = Columns.id.prefix("a")
|
||||
val afileMeta = fileId.prefix("a")
|
||||
val bPos = RAttachment.Columns.position.prefix("b")
|
||||
val bId = RAttachment.Columns.id.prefix("b")
|
||||
val bItem = RAttachment.Columns.itemId.prefix("b")
|
||||
val mId = RFileMeta.Columns.id.prefix("m")
|
||||
val a = RAttachmentPreview.as("a")
|
||||
val b = RAttachment.as("b")
|
||||
val m = RFileMeta.as("m")
|
||||
|
||||
val cols = all.map(_.prefix("a")) ++ RFileMeta.Columns.all.map(_.prefix("m"))
|
||||
val from = table ++ fr"a INNER JOIN" ++
|
||||
RFileMeta.table ++ fr"m ON" ++ afileMeta.is(mId) ++ fr"INNER JOIN" ++
|
||||
RAttachment.table ++ fr"b ON" ++ aId.is(bId)
|
||||
val where = bItem.is(id)
|
||||
|
||||
(selectSimple(cols, from, where) ++ orderBy(bPos.asc))
|
||||
.query[(RAttachmentPreview, FileMeta)]
|
||||
.to[Vector]
|
||||
Select(
|
||||
select(a.all, m.all),
|
||||
from(a)
|
||||
.innerJoin(m, a.fileId === m.id)
|
||||
.innerJoin(b, b.id === a.id),
|
||||
b.itemId === id
|
||||
).orderBy(b.position.asc).build.query[(RAttachmentPreview, FileMeta)].to[Vector]
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,8 +1,10 @@
|
||||
package docspell.store.records
|
||||
|
||||
import cats.data.NonEmptyList
|
||||
|
||||
import docspell.common._
|
||||
import docspell.store.impl.Implicits._
|
||||
import docspell.store.impl._
|
||||
import docspell.store.qb.DSL._
|
||||
import docspell.store.qb._
|
||||
|
||||
import bitpeace.FileMeta
|
||||
import doobie._
|
||||
@ -19,79 +21,70 @@ case class RAttachmentSource(
|
||||
)
|
||||
|
||||
object RAttachmentSource {
|
||||
final case class Table(alias: Option[String]) extends TableDef {
|
||||
val tableName = "attachment_source"
|
||||
|
||||
val table = fr"attachment_source"
|
||||
val id = Column[Ident]("id", this)
|
||||
val fileId = Column[Ident]("file_id", this)
|
||||
val name = Column[String]("filename", this)
|
||||
val created = Column[Timestamp]("created", this)
|
||||
|
||||
object Columns {
|
||||
val id = Column("id")
|
||||
val fileId = Column("file_id")
|
||||
val name = Column("filename")
|
||||
val created = Column("created")
|
||||
|
||||
val all = List(id, fileId, name, created)
|
||||
val all = NonEmptyList.of[Column[_]](id, fileId, name, created)
|
||||
}
|
||||
|
||||
import Columns._
|
||||
val T = Table(None)
|
||||
def as(alias: String): Table =
|
||||
Table(Some(alias))
|
||||
|
||||
def of(ra: RAttachment): RAttachmentSource =
|
||||
RAttachmentSource(ra.id, ra.fileId, ra.name, ra.created)
|
||||
|
||||
def insert(v: RAttachmentSource): ConnectionIO[Int] =
|
||||
insertRow(table, all, fr"${v.id},${v.fileId},${v.name},${v.created}").update.run
|
||||
DML.insert(T, T.all, fr"${v.id},${v.fileId},${v.name},${v.created}")
|
||||
|
||||
def findById(attachId: Ident): ConnectionIO[Option[RAttachmentSource]] =
|
||||
selectSimple(all, table, id.is(attachId)).query[RAttachmentSource].option
|
||||
run(select(T.all), from(T), T.id === attachId).query[RAttachmentSource].option
|
||||
|
||||
def isSameFile(attachId: Ident, file: Ident): ConnectionIO[Boolean] =
|
||||
selectCount(id, table, and(id.is(attachId), fileId.is(file)))
|
||||
Select(count(T.id).s, from(T), T.id === attachId && T.fileId === file).build
|
||||
.query[Int]
|
||||
.unique
|
||||
.map(_ > 0)
|
||||
|
||||
def isConverted(attachId: Ident): ConnectionIO[Boolean] = {
|
||||
val sId = Columns.id.prefix("s")
|
||||
val sFile = Columns.fileId.prefix("s")
|
||||
val aId = RAttachment.Columns.id.prefix("a")
|
||||
val aFile = RAttachment.Columns.fileId.prefix("a")
|
||||
|
||||
val from = table ++ fr"s INNER JOIN" ++
|
||||
RAttachment.table ++ fr"a ON" ++ aId.is(sId)
|
||||
|
||||
selectCount(aId, from, and(aId.is(attachId), aFile.isNot(sFile)))
|
||||
.query[Int]
|
||||
.unique
|
||||
.map(_ > 0)
|
||||
val s = RAttachmentSource.as("s")
|
||||
val a = RAttachment.as("a")
|
||||
Select(
|
||||
count(a.id).s,
|
||||
from(s).innerJoin(a, a.id === s.id),
|
||||
a.id === attachId && a.fileId <> s.fileId
|
||||
).build.query[Int].unique.map(_ > 0)
|
||||
}
|
||||
|
||||
def delete(attachId: Ident): ConnectionIO[Int] =
|
||||
deleteFrom(table, id.is(attachId)).update.run
|
||||
DML.delete(T, T.id === attachId)
|
||||
|
||||
def findByIdAndCollective(
|
||||
attachId: Ident,
|
||||
collective: Ident
|
||||
): ConnectionIO[Option[RAttachmentSource]] = {
|
||||
val bId = RAttachment.Columns.id.prefix("b")
|
||||
val aId = Columns.id.prefix("a")
|
||||
val bItem = RAttachment.Columns.itemId.prefix("b")
|
||||
val iId = RItem.Columns.id.prefix("i")
|
||||
val iColl = RItem.Columns.cid.prefix("i")
|
||||
val b = RAttachment.as("b")
|
||||
val a = RAttachmentSource.as("a")
|
||||
val i = RItem.as("i")
|
||||
|
||||
val from = table ++ fr"a INNER JOIN" ++
|
||||
RAttachment.table ++ fr"b ON" ++ aId.is(bId) ++
|
||||
fr"INNER JOIN" ++ RItem.table ++ fr"i ON" ++ bItem.is(iId)
|
||||
|
||||
val where = and(aId.is(attachId), bId.is(attachId), iColl.is(collective))
|
||||
|
||||
selectSimple(all.map(_.prefix("a")), from, where).query[RAttachmentSource].option
|
||||
Select(
|
||||
select(a.all),
|
||||
from(a)
|
||||
.innerJoin(b, a.id === b.id)
|
||||
.innerJoin(i, i.id === b.itemId),
|
||||
a.id === attachId && b.id === attachId && i.cid === collective
|
||||
).build.query[RAttachmentSource].option
|
||||
}
|
||||
|
||||
def findByItem(itemId: Ident): ConnectionIO[Vector[RAttachmentSource]] = {
|
||||
val sId = Columns.id.prefix("s")
|
||||
val aId = RAttachment.Columns.id.prefix("a")
|
||||
val aItem = RAttachment.Columns.itemId.prefix("a")
|
||||
|
||||
val from = table ++ fr"s INNER JOIN" ++ RAttachment.table ++ fr"a ON" ++ sId.is(aId)
|
||||
selectSimple(all.map(_.prefix("s")), from, aItem.is(itemId))
|
||||
val s = RAttachmentSource.as("s")
|
||||
val a = RAttachment.as("a")
|
||||
Select(select(s.all), from(s).innerJoin(a, a.id === s.id), a.itemId === itemId).build
|
||||
.query[RAttachmentSource]
|
||||
.to[Vector]
|
||||
}
|
||||
@ -101,22 +94,17 @@ object RAttachmentSource {
|
||||
): ConnectionIO[Vector[(RAttachmentSource, FileMeta)]] = {
|
||||
import bitpeace.sql._
|
||||
|
||||
val aId = Columns.id.prefix("a")
|
||||
val afileMeta = fileId.prefix("a")
|
||||
val bPos = RAttachment.Columns.position.prefix("b")
|
||||
val bId = RAttachment.Columns.id.prefix("b")
|
||||
val bItem = RAttachment.Columns.itemId.prefix("b")
|
||||
val mId = RFileMeta.Columns.id.prefix("m")
|
||||
val a = RAttachmentSource.as("a")
|
||||
val b = RAttachment.as("b")
|
||||
val m = RFileMeta.as("m")
|
||||
|
||||
val cols = all.map(_.prefix("a")) ++ RFileMeta.Columns.all.map(_.prefix("m"))
|
||||
val from = table ++ fr"a INNER JOIN" ++
|
||||
RFileMeta.table ++ fr"m ON" ++ afileMeta.is(mId) ++ fr"INNER JOIN" ++
|
||||
RAttachment.table ++ fr"b ON" ++ aId.is(bId)
|
||||
val where = bItem.is(id)
|
||||
|
||||
(selectSimple(cols, from, where) ++ orderBy(bPos.asc))
|
||||
.query[(RAttachmentSource, FileMeta)]
|
||||
.to[Vector]
|
||||
Select(
|
||||
select(a.all, m.all),
|
||||
from(a)
|
||||
.innerJoin(m, a.fileId === m.id)
|
||||
.innerJoin(b, b.id === a.id),
|
||||
b.itemId === id
|
||||
).orderBy(b.position.asc).build.query[(RAttachmentSource, FileMeta)].to[Vector]
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,10 +1,11 @@
|
||||
package docspell.store.records
|
||||
|
||||
import cats.data.NonEmptyList
|
||||
import cats.implicits._
|
||||
|
||||
import docspell.common._
|
||||
import docspell.store.impl.Implicits._
|
||||
import docspell.store.impl._
|
||||
import docspell.store.qb.DSL._
|
||||
import docspell.store.qb._
|
||||
|
||||
import com.github.eikek.calev._
|
||||
import doobie._
|
||||
@ -21,71 +22,69 @@ case class RClassifierSetting(
|
||||
) {}
|
||||
|
||||
object RClassifierSetting {
|
||||
final case class Table(alias: Option[String]) extends TableDef {
|
||||
val tableName = "classifier_setting"
|
||||
|
||||
val table = fr"classifier_setting"
|
||||
|
||||
object Columns {
|
||||
val cid = Column("cid")
|
||||
val enabled = Column("enabled")
|
||||
val schedule = Column("schedule")
|
||||
val category = Column("category")
|
||||
val itemCount = Column("item_count")
|
||||
val fileId = Column("file_id")
|
||||
val created = Column("created")
|
||||
val all = List(cid, enabled, schedule, category, itemCount, fileId, created)
|
||||
val cid = Column[Ident]("cid", this)
|
||||
val enabled = Column[Boolean]("enabled", this)
|
||||
val schedule = Column[CalEvent]("schedule", this)
|
||||
val category = Column[String]("category", this)
|
||||
val itemCount = Column[Int]("item_count", this)
|
||||
val fileId = Column[Ident]("file_id", this)
|
||||
val created = Column[Timestamp]("created", this)
|
||||
val all = NonEmptyList
|
||||
.of[Column[_]](cid, enabled, schedule, category, itemCount, fileId, created)
|
||||
}
|
||||
import Columns._
|
||||
|
||||
def insert(v: RClassifierSetting): ConnectionIO[Int] = {
|
||||
val sql =
|
||||
insertRow(
|
||||
table,
|
||||
all,
|
||||
val T = Table(None)
|
||||
def as(alias: String): Table =
|
||||
Table(Some(alias))
|
||||
|
||||
def insert(v: RClassifierSetting): ConnectionIO[Int] =
|
||||
DML.insert(
|
||||
T,
|
||||
T.all,
|
||||
fr"${v.cid},${v.enabled},${v.schedule},${v.category},${v.itemCount},${v.fileId},${v.created}"
|
||||
)
|
||||
sql.update.run
|
||||
}
|
||||
|
||||
def updateAll(v: RClassifierSetting): ConnectionIO[Int] = {
|
||||
val sql = updateRow(
|
||||
table,
|
||||
cid.is(v.cid),
|
||||
commas(
|
||||
enabled.setTo(v.enabled),
|
||||
schedule.setTo(v.schedule),
|
||||
category.setTo(v.category),
|
||||
itemCount.setTo(v.itemCount),
|
||||
fileId.setTo(v.fileId)
|
||||
def updateAll(v: RClassifierSetting): ConnectionIO[Int] =
|
||||
DML.update(
|
||||
T,
|
||||
T.cid === v.cid,
|
||||
DML.set(
|
||||
T.enabled.setTo(v.enabled),
|
||||
T.schedule.setTo(v.schedule),
|
||||
T.category.setTo(v.category),
|
||||
T.itemCount.setTo(v.itemCount),
|
||||
T.fileId.setTo(v.fileId)
|
||||
)
|
||||
)
|
||||
sql.update.run
|
||||
}
|
||||
|
||||
def updateFile(coll: Ident, fid: Ident): ConnectionIO[Int] =
|
||||
updateRow(table, cid.is(coll), fileId.setTo(fid)).update.run
|
||||
DML.update(T, T.cid === coll, DML.set(T.fileId.setTo(fid)))
|
||||
|
||||
def updateSettings(v: RClassifierSetting): ConnectionIO[Int] =
|
||||
for {
|
||||
n1 <- updateRow(
|
||||
table,
|
||||
cid.is(v.cid),
|
||||
commas(
|
||||
enabled.setTo(v.enabled),
|
||||
schedule.setTo(v.schedule),
|
||||
itemCount.setTo(v.itemCount),
|
||||
category.setTo(v.category)
|
||||
n1 <- DML.update(
|
||||
T,
|
||||
T.cid === v.cid,
|
||||
DML.set(
|
||||
T.enabled.setTo(v.enabled),
|
||||
T.schedule.setTo(v.schedule),
|
||||
T.itemCount.setTo(v.itemCount),
|
||||
T.category.setTo(v.category)
|
||||
)
|
||||
)
|
||||
).update.run
|
||||
n2 <- if (n1 <= 0) insert(v) else 0.pure[ConnectionIO]
|
||||
} yield n1 + n2
|
||||
|
||||
def findById(id: Ident): ConnectionIO[Option[RClassifierSetting]] = {
|
||||
val sql = selectSimple(all, table, cid.is(id))
|
||||
val sql = run(select(T.all), from(T), T.cid === id)
|
||||
sql.query[RClassifierSetting].option
|
||||
}
|
||||
|
||||
def delete(coll: Ident): ConnectionIO[Int] =
|
||||
deleteFrom(table, cid.is(coll)).update.run
|
||||
DML.delete(T, T.cid === coll)
|
||||
|
||||
case class Classifier(
|
||||
enabled: Boolean,
|
||||
|
@ -1,10 +1,11 @@
|
||||
package docspell.store.records
|
||||
|
||||
import cats.data.NonEmptyList
|
||||
import fs2.Stream
|
||||
|
||||
import docspell.common._
|
||||
import docspell.store.impl.Column
|
||||
import docspell.store.impl.Implicits._
|
||||
import docspell.store.qb.DSL._
|
||||
import docspell.store.qb._
|
||||
|
||||
import doobie._
|
||||
import doobie.implicits._
|
||||
@ -18,58 +19,54 @@ case class RCollective(
|
||||
)
|
||||
|
||||
object RCollective {
|
||||
final case class Table(alias: Option[String]) extends TableDef {
|
||||
val tableName = "collective"
|
||||
|
||||
val table = fr"collective"
|
||||
val id = Column[Ident]("cid", this)
|
||||
val state = Column[CollectiveState]("state", this)
|
||||
val language = Column[Language]("doclang", this)
|
||||
val integration = Column[Boolean]("integration_enabled", this)
|
||||
val created = Column[Timestamp]("created", this)
|
||||
|
||||
object Columns {
|
||||
|
||||
val id = Column("cid")
|
||||
val state = Column("state")
|
||||
val language = Column("doclang")
|
||||
val integration = Column("integration_enabled")
|
||||
val created = Column("created")
|
||||
|
||||
val all = List(id, state, language, integration, created)
|
||||
val all = NonEmptyList.of[Column[_]](id, state, language, integration, created)
|
||||
}
|
||||
|
||||
import Columns._
|
||||
val T = Table(None)
|
||||
def as(alias: String): Table =
|
||||
Table(Some(alias))
|
||||
|
||||
def insert(value: RCollective): ConnectionIO[Int] = {
|
||||
val sql = insertRow(
|
||||
table,
|
||||
Columns.all,
|
||||
def insert(value: RCollective): ConnectionIO[Int] =
|
||||
DML.insert(
|
||||
T,
|
||||
T.all,
|
||||
fr"${value.id},${value.state},${value.language},${value.integrationEnabled},${value.created}"
|
||||
)
|
||||
sql.update.run
|
||||
}
|
||||
|
||||
def update(value: RCollective): ConnectionIO[Int] = {
|
||||
val sql = updateRow(
|
||||
table,
|
||||
id.is(value.id),
|
||||
commas(
|
||||
state.setTo(value.state)
|
||||
def update(value: RCollective): ConnectionIO[Int] =
|
||||
DML.update(
|
||||
T,
|
||||
T.id === value.id,
|
||||
DML.set(
|
||||
T.state.setTo(value.state)
|
||||
)
|
||||
)
|
||||
sql.update.run
|
||||
}
|
||||
|
||||
def findLanguage(cid: Ident): ConnectionIO[Option[Language]] =
|
||||
selectSimple(List(language), table, id.is(cid)).query[Option[Language]].unique
|
||||
Select(T.language.s, from(T), T.id === cid).build.query[Option[Language]].unique
|
||||
|
||||
def updateLanguage(cid: Ident, lang: Language): ConnectionIO[Int] =
|
||||
updateRow(table, id.is(cid), language.setTo(lang)).update.run
|
||||
DML.update(T, T.id === cid, DML.set(T.language.setTo(lang)))
|
||||
|
||||
def updateSettings(cid: Ident, settings: Settings): ConnectionIO[Int] =
|
||||
for {
|
||||
n1 <- updateRow(
|
||||
table,
|
||||
id.is(cid),
|
||||
commas(
|
||||
language.setTo(settings.language),
|
||||
integration.setTo(settings.integrationEnabled)
|
||||
n1 <- DML.update(
|
||||
T,
|
||||
T.id === cid,
|
||||
DML.set(
|
||||
T.language.setTo(settings.language),
|
||||
T.integration.setTo(settings.integrationEnabled)
|
||||
)
|
||||
)
|
||||
).update.run
|
||||
cls <-
|
||||
Timestamp
|
||||
.current[ConnectionIO]
|
||||
@ -83,66 +80,64 @@ object RCollective {
|
||||
} yield n1 + n2
|
||||
|
||||
def getSettings(coll: Ident): ConnectionIO[Option[Settings]] = {
|
||||
val cId = id.prefix("c")
|
||||
val CS = RClassifierSetting.Columns
|
||||
val csCid = CS.cid.prefix("cs")
|
||||
val c = RCollective.as("c")
|
||||
val cs = RClassifierSetting.as("cs")
|
||||
|
||||
val cols = Seq(
|
||||
language.prefix("c"),
|
||||
integration.prefix("c"),
|
||||
CS.enabled.prefix("cs"),
|
||||
CS.schedule.prefix("cs"),
|
||||
CS.itemCount.prefix("cs"),
|
||||
CS.category.prefix("cs")
|
||||
)
|
||||
val from = table ++ fr"c LEFT JOIN" ++
|
||||
RClassifierSetting.table ++ fr"cs ON" ++ csCid.is(cId)
|
||||
|
||||
selectSimple(cols, from, cId.is(coll))
|
||||
.query[Settings]
|
||||
.option
|
||||
Select(
|
||||
select(
|
||||
c.language.s,
|
||||
c.integration.s,
|
||||
cs.enabled.s,
|
||||
cs.schedule.s,
|
||||
cs.itemCount.s,
|
||||
cs.category.s
|
||||
),
|
||||
from(c).leftJoin(cs, cs.cid === c.id),
|
||||
c.id === coll
|
||||
).build.query[Settings].option
|
||||
}
|
||||
|
||||
def findById(cid: Ident): ConnectionIO[Option[RCollective]] = {
|
||||
val sql = selectSimple(all, table, id.is(cid))
|
||||
val sql = run(select(T.all), from(T), T.id === cid)
|
||||
sql.query[RCollective].option
|
||||
}
|
||||
|
||||
def findByItem(itemId: Ident): ConnectionIO[Option[RCollective]] = {
|
||||
val iColl = RItem.Columns.cid.prefix("i")
|
||||
val iId = RItem.Columns.id.prefix("i")
|
||||
val cId = id.prefix("c")
|
||||
val from = RItem.table ++ fr"i INNER JOIN" ++ table ++ fr"c ON" ++ iColl.is(cId)
|
||||
selectSimple(all.map(_.prefix("c")), from, iId.is(itemId)).query[RCollective].option
|
||||
val i = RItem.as("i")
|
||||
val c = RCollective.as("c")
|
||||
Select(
|
||||
select(c.all),
|
||||
from(i).innerJoin(c, i.cid === c.id),
|
||||
i.id === itemId
|
||||
).build.query[RCollective].option
|
||||
}
|
||||
|
||||
def existsById(cid: Ident): ConnectionIO[Boolean] = {
|
||||
val sql = selectCount(id, table, id.is(cid))
|
||||
val sql = Select(count(T.id).s, from(T), T.id === cid).build
|
||||
sql.query[Int].unique.map(_ > 0)
|
||||
}
|
||||
|
||||
def findAll(order: Columns.type => Column): ConnectionIO[Vector[RCollective]] = {
|
||||
val sql = selectSimple(all, table, Fragment.empty) ++ orderBy(order(Columns).f)
|
||||
sql.query[RCollective].to[Vector]
|
||||
def findAll(order: Table => Column[_]): ConnectionIO[Vector[RCollective]] = {
|
||||
val sql = Select(select(T.all), from(T)).orderBy(order(T))
|
||||
sql.build.query[RCollective].to[Vector]
|
||||
}
|
||||
|
||||
def streamAll(order: Columns.type => Column): Stream[ConnectionIO, RCollective] = {
|
||||
val sql = selectSimple(all, table, Fragment.empty) ++ orderBy(order(Columns).f)
|
||||
sql.query[RCollective].stream
|
||||
def streamAll(order: Table => Column[_]): Stream[ConnectionIO, RCollective] = {
|
||||
val sql = Select(select(T.all), from(T)).orderBy(order(T))
|
||||
sql.build.query[RCollective].stream
|
||||
}
|
||||
|
||||
def findByAttachment(attachId: Ident): ConnectionIO[Option[RCollective]] = {
|
||||
val iColl = RItem.Columns.cid.prefix("i")
|
||||
val iId = RItem.Columns.id.prefix("i")
|
||||
val aItem = RAttachment.Columns.itemId.prefix("a")
|
||||
val aId = RAttachment.Columns.id.prefix("a")
|
||||
val cId = Columns.id.prefix("c")
|
||||
|
||||
val from = table ++ fr"c INNER JOIN" ++
|
||||
RItem.table ++ fr"i ON" ++ cId.is(iColl) ++ fr"INNER JOIN" ++
|
||||
RAttachment.table ++ fr"a ON" ++ aItem.is(iId)
|
||||
|
||||
selectSimple(all.map(_.prefix("c")), from, aId.is(attachId)).query[RCollective].option
|
||||
val i = RItem.as("i")
|
||||
val a = RAttachment.as("a")
|
||||
val c = RCollective.as("c")
|
||||
Select(
|
||||
select(c.all),
|
||||
from(c)
|
||||
.innerJoin(i, c.id === i.cid)
|
||||
.innerJoin(a, a.itemId === i.id),
|
||||
a.id === attachId
|
||||
).build.query[RCollective].option
|
||||
}
|
||||
|
||||
case class Settings(
|
||||
|
@ -1,8 +1,10 @@
|
||||
package docspell.store.records
|
||||
|
||||
import cats.data.NonEmptyList
|
||||
|
||||
import docspell.common._
|
||||
import docspell.store.impl.Implicits._
|
||||
import docspell.store.impl._
|
||||
import docspell.store.qb.DSL._
|
||||
import docspell.store.qb._
|
||||
|
||||
import doobie._
|
||||
import doobie.implicits._
|
||||
@ -18,64 +20,62 @@ case class RContact(
|
||||
|
||||
object RContact {
|
||||
|
||||
val table = fr"contact"
|
||||
final case class Table(alias: Option[String]) extends TableDef {
|
||||
val tableName = "contact"
|
||||
|
||||
object Columns {
|
||||
val contactId = Column("contactid")
|
||||
val value = Column("value")
|
||||
val kind = Column("kind")
|
||||
val personId = Column("pid")
|
||||
val orgId = Column("oid")
|
||||
val created = Column("created")
|
||||
val all = List(contactId, value, kind, personId, orgId, created)
|
||||
val contactId = Column[Ident]("contactid", this)
|
||||
val value = Column[String]("value", this)
|
||||
val kind = Column[ContactKind]("kind", this)
|
||||
val personId = Column[Ident]("pid", this)
|
||||
val orgId = Column[Ident]("oid", this)
|
||||
val created = Column[Timestamp]("created", this)
|
||||
val all = NonEmptyList.of[Column[_]](contactId, value, kind, personId, orgId, created)
|
||||
}
|
||||
|
||||
import Columns._
|
||||
private val T = Table(None)
|
||||
def as(alias: String): Table =
|
||||
Table(Some(alias))
|
||||
|
||||
def insert(v: RContact): ConnectionIO[Int] = {
|
||||
val sql = insertRow(
|
||||
table,
|
||||
all,
|
||||
def insert(v: RContact): ConnectionIO[Int] =
|
||||
DML.insert(
|
||||
T,
|
||||
T.all,
|
||||
fr"${v.contactId},${v.value},${v.kind},${v.personId},${v.orgId},${v.created}"
|
||||
)
|
||||
sql.update.run
|
||||
}
|
||||
|
||||
def update(v: RContact): ConnectionIO[Int] = {
|
||||
val sql = updateRow(
|
||||
table,
|
||||
contactId.is(v.contactId),
|
||||
commas(
|
||||
value.setTo(v.value),
|
||||
kind.setTo(v.kind),
|
||||
personId.setTo(v.personId),
|
||||
orgId.setTo(v.orgId)
|
||||
def update(v: RContact): ConnectionIO[Int] =
|
||||
DML.update(
|
||||
T,
|
||||
T.contactId === v.contactId,
|
||||
DML.set(
|
||||
T.value.setTo(v.value),
|
||||
T.kind.setTo(v.kind),
|
||||
T.personId.setTo(v.personId),
|
||||
T.orgId.setTo(v.orgId)
|
||||
)
|
||||
)
|
||||
sql.update.run
|
||||
}
|
||||
|
||||
def delete(v: RContact): ConnectionIO[Int] =
|
||||
deleteFrom(table, contactId.is(v.contactId)).update.run
|
||||
DML.delete(T, T.contactId === v.contactId)
|
||||
|
||||
def deleteOrg(oid: Ident): ConnectionIO[Int] =
|
||||
deleteFrom(table, orgId.is(oid)).update.run
|
||||
DML.delete(T, T.orgId === oid)
|
||||
|
||||
def deletePerson(pid: Ident): ConnectionIO[Int] =
|
||||
deleteFrom(table, personId.is(pid)).update.run
|
||||
DML.delete(T, T.personId === pid)
|
||||
|
||||
def findById(id: Ident): ConnectionIO[Option[RContact]] = {
|
||||
val sql = selectSimple(all, table, contactId.is(id))
|
||||
val sql = run(select(T.all), from(T), T.contactId === id)
|
||||
sql.query[RContact].option
|
||||
}
|
||||
|
||||
def findAllPerson(pid: Ident): ConnectionIO[Vector[RContact]] = {
|
||||
val sql = selectSimple(all, table, personId.is(pid))
|
||||
val sql = run(select(T.all), from(T), T.personId === pid)
|
||||
sql.query[RContact].to[Vector]
|
||||
}
|
||||
|
||||
def findAllOrg(oid: Ident): ConnectionIO[Vector[RContact]] = {
|
||||
val sql = selectSimple(all, table, orgId.is(oid))
|
||||
val sql = run(select(T.all), from(T), T.orgId === oid)
|
||||
sql.query[RContact].to[Vector]
|
||||
}
|
||||
}
|
||||
|
@ -1,10 +1,11 @@
|
||||
package docspell.store.records
|
||||
|
||||
import cats.data.NonEmptyList
|
||||
import cats.implicits._
|
||||
|
||||
import docspell.common._
|
||||
import docspell.store.impl.Column
|
||||
import docspell.store.impl.Implicits._
|
||||
import docspell.store.qb.DSL._
|
||||
import docspell.store.qb._
|
||||
|
||||
import doobie._
|
||||
import doobie.implicits._
|
||||
@ -19,58 +20,63 @@ case class RCustomField(
|
||||
)
|
||||
|
||||
object RCustomField {
|
||||
final case class Table(alias: Option[String]) extends TableDef {
|
||||
val tableName = "custom_field"
|
||||
|
||||
val table = fr"custom_field"
|
||||
val id = Column[Ident]("id", this)
|
||||
val name = Column[Ident]("name", this)
|
||||
val label = Column[String]("label", this)
|
||||
val cid = Column[Ident]("cid", this)
|
||||
val ftype = Column[CustomFieldType]("ftype", this)
|
||||
val created = Column[Timestamp]("created", this)
|
||||
|
||||
object Columns {
|
||||
|
||||
val id = Column("id")
|
||||
val name = Column("name")
|
||||
val label = Column("label")
|
||||
val cid = Column("cid")
|
||||
val ftype = Column("ftype")
|
||||
val created = Column("created")
|
||||
|
||||
val all = List(id, name, label, cid, ftype, created)
|
||||
val all = NonEmptyList.of[Column[_]](id, name, label, cid, ftype, created)
|
||||
}
|
||||
import Columns._
|
||||
|
||||
def insert(value: RCustomField): ConnectionIO[Int] = {
|
||||
val sql = insertRow(
|
||||
table,
|
||||
Columns.all,
|
||||
val T = Table(None)
|
||||
def as(alias: String): Table =
|
||||
Table(Some(alias))
|
||||
|
||||
def insert(value: RCustomField): ConnectionIO[Int] =
|
||||
DML.insert(
|
||||
T,
|
||||
T.all,
|
||||
fr"${value.id},${value.name},${value.label},${value.cid},${value.ftype},${value.created}"
|
||||
)
|
||||
sql.update.run
|
||||
}
|
||||
|
||||
def exists(fname: Ident, coll: Ident): ConnectionIO[Boolean] =
|
||||
selectCount(id, table, and(name.is(fname), cid.is(coll))).query[Int].unique.map(_ > 0)
|
||||
run(select(count(T.id)), from(T), T.name === fname && T.cid === coll)
|
||||
.query[Int]
|
||||
.unique
|
||||
.map(_ > 0)
|
||||
|
||||
def findById(fid: Ident, coll: Ident): ConnectionIO[Option[RCustomField]] =
|
||||
selectSimple(all, table, and(id.is(fid), cid.is(coll))).query[RCustomField].option
|
||||
run(select(T.all), from(T), T.id === fid && T.cid === coll).query[RCustomField].option
|
||||
|
||||
def findByIdOrName(idOrName: Ident, coll: Ident): ConnectionIO[Option[RCustomField]] =
|
||||
selectSimple(all, table, and(cid.is(coll), or(id.is(idOrName), name.is(idOrName))))
|
||||
.query[RCustomField]
|
||||
.option
|
||||
Select(
|
||||
select(T.all),
|
||||
from(T),
|
||||
T.cid === coll && (T.id === idOrName || T.name === idOrName)
|
||||
).build.query[RCustomField].option
|
||||
|
||||
def deleteById(fid: Ident, coll: Ident): ConnectionIO[Int] =
|
||||
deleteFrom(table, and(id.is(fid), cid.is(coll))).update.run
|
||||
DML.delete(T, T.id === fid && T.cid === coll)
|
||||
|
||||
def findAll(coll: Ident): ConnectionIO[Vector[RCustomField]] =
|
||||
selectSimple(all, table, cid.is(coll)).query[RCustomField].to[Vector]
|
||||
run(select(T.all), from(T), T.cid === coll).query[RCustomField].to[Vector]
|
||||
|
||||
def update(value: RCustomField): ConnectionIO[Int] =
|
||||
updateRow(
|
||||
table,
|
||||
and(id.is(value.id), cid.is(value.cid)),
|
||||
commas(
|
||||
name.setTo(value.name),
|
||||
label.setTo(value.label),
|
||||
ftype.setTo(value.ftype)
|
||||
DML
|
||||
.update(
|
||||
T,
|
||||
T.id === value.id && T.cid === value.cid,
|
||||
DML.set(
|
||||
T.name.setTo(value.name),
|
||||
T.label.setTo(value.label),
|
||||
T.ftype.setTo(value.ftype)
|
||||
)
|
||||
)
|
||||
).update.run
|
||||
|
||||
def setValue(f: RCustomField, item: Ident, fval: String): ConnectionIO[Int] =
|
||||
for {
|
||||
|
@ -3,8 +3,8 @@ package docspell.store.records
|
||||
import cats.data.NonEmptyList
|
||||
|
||||
import docspell.common._
|
||||
import docspell.store.impl.Column
|
||||
import docspell.store.impl.Implicits._
|
||||
import docspell.store.qb.DSL._
|
||||
import docspell.store.qb._
|
||||
|
||||
import doobie._
|
||||
import doobie.implicits._
|
||||
@ -17,51 +17,51 @@ case class RCustomFieldValue(
|
||||
)
|
||||
|
||||
object RCustomFieldValue {
|
||||
final case class Table(alias: Option[String]) extends TableDef {
|
||||
val tableName = "custom_field_value"
|
||||
|
||||
val table = fr"custom_field_value"
|
||||
val id = Column[Ident]("id", this)
|
||||
val itemId = Column[Ident]("item_id", this)
|
||||
val field = Column[Ident]("field", this)
|
||||
val value = Column[String]("field_value", this)
|
||||
|
||||
object Columns {
|
||||
|
||||
val id = Column("id")
|
||||
val itemId = Column("item_id")
|
||||
val field = Column("field")
|
||||
val value = Column("field_value")
|
||||
|
||||
val all = List(id, itemId, field, value)
|
||||
val all = NonEmptyList.of[Column[_]](id, itemId, field, value)
|
||||
}
|
||||
|
||||
def insert(value: RCustomFieldValue): ConnectionIO[Int] = {
|
||||
val sql = insertRow(
|
||||
table,
|
||||
Columns.all,
|
||||
val T = Table(None)
|
||||
def as(alias: String): Table =
|
||||
Table(Some(alias))
|
||||
|
||||
def insert(value: RCustomFieldValue): ConnectionIO[Int] =
|
||||
DML.insert(
|
||||
T,
|
||||
T.all,
|
||||
fr"${value.id},${value.itemId},${value.field},${value.value}"
|
||||
)
|
||||
sql.update.run
|
||||
}
|
||||
|
||||
def updateValue(
|
||||
fieldId: Ident,
|
||||
item: Ident,
|
||||
value: String
|
||||
): ConnectionIO[Int] =
|
||||
updateRow(
|
||||
table,
|
||||
and(Columns.itemId.is(item), Columns.field.is(fieldId)),
|
||||
Columns.value.setTo(value)
|
||||
).update.run
|
||||
DML.update(
|
||||
T,
|
||||
T.itemId === item && T.field === fieldId,
|
||||
DML.set(T.value.setTo(value))
|
||||
)
|
||||
|
||||
def countField(fieldId: Ident): ConnectionIO[Int] =
|
||||
selectCount(Columns.id, table, Columns.field.is(fieldId)).query[Int].unique
|
||||
Select(count(T.id).s, from(T), T.field === fieldId).build.query[Int].unique
|
||||
|
||||
def deleteByField(fieldId: Ident): ConnectionIO[Int] =
|
||||
deleteFrom(table, Columns.field.is(fieldId)).update.run
|
||||
DML.delete(T, T.field === fieldId)
|
||||
|
||||
def deleteByItem(item: Ident): ConnectionIO[Int] =
|
||||
deleteFrom(table, Columns.itemId.is(item)).update.run
|
||||
DML.delete(T, T.itemId === item)
|
||||
|
||||
def deleteValue(fieldId: Ident, items: NonEmptyList[Ident]): ConnectionIO[Int] =
|
||||
deleteFrom(
|
||||
table,
|
||||
and(Columns.field.is(fieldId), Columns.itemId.isIn(items))
|
||||
).update.run
|
||||
DML.delete(
|
||||
T,
|
||||
T.field === fieldId && T.itemId.in(items)
|
||||
)
|
||||
}
|
||||
|
@ -1,8 +1,10 @@
|
||||
package docspell.store.records
|
||||
|
||||
import cats.data.NonEmptyList
|
||||
|
||||
import docspell.common._
|
||||
import docspell.store.impl.Implicits._
|
||||
import docspell.store.impl._
|
||||
import docspell.store.qb.DSL._
|
||||
import docspell.store.qb._
|
||||
|
||||
import doobie._
|
||||
import doobie.implicits._
|
||||
@ -16,70 +18,84 @@ case class REquipment(
|
||||
) {}
|
||||
|
||||
object REquipment {
|
||||
final case class Table(alias: Option[String]) extends TableDef {
|
||||
val tableName = "equipment"
|
||||
|
||||
val table = fr"equipment"
|
||||
|
||||
object Columns {
|
||||
val eid = Column("eid")
|
||||
val cid = Column("cid")
|
||||
val name = Column("name")
|
||||
val created = Column("created")
|
||||
val updated = Column("updated")
|
||||
val all = List(eid, cid, name, created, updated)
|
||||
val eid = Column[Ident]("eid", this)
|
||||
val cid = Column[Ident]("cid", this)
|
||||
val name = Column[String]("name", this)
|
||||
val created = Column[Timestamp]("created", this)
|
||||
val updated = Column[Timestamp]("updated", this)
|
||||
val all = NonEmptyList.of[Column[_]](eid, cid, name, created, updated)
|
||||
}
|
||||
import Columns._
|
||||
|
||||
val T = Table(None)
|
||||
def as(alias: String): Table =
|
||||
Table(Some(alias))
|
||||
|
||||
def insert(v: REquipment): ConnectionIO[Int] = {
|
||||
val sql =
|
||||
insertRow(table, all, fr"${v.eid},${v.cid},${v.name},${v.created},${v.updated}")
|
||||
sql.update.run
|
||||
val t = Table(None)
|
||||
DML
|
||||
.insert(
|
||||
t,
|
||||
t.all,
|
||||
fr"${v.eid},${v.cid},${v.name},${v.created},${v.updated}"
|
||||
)
|
||||
}
|
||||
|
||||
def update(v: REquipment): ConnectionIO[Int] = {
|
||||
def sql(now: Timestamp) =
|
||||
updateRow(
|
||||
table,
|
||||
and(eid.is(v.eid), cid.is(v.cid)),
|
||||
commas(
|
||||
cid.setTo(v.cid),
|
||||
name.setTo(v.name),
|
||||
updated.setTo(now)
|
||||
)
|
||||
)
|
||||
val t = Table(None)
|
||||
for {
|
||||
now <- Timestamp.current[ConnectionIO]
|
||||
n <- sql(now).update.run
|
||||
n <- DML
|
||||
.update(
|
||||
t,
|
||||
where(t.eid === v.eid, t.cid === v.cid),
|
||||
DML.set(
|
||||
t.cid.setTo(v.cid),
|
||||
t.name.setTo(v.name),
|
||||
t.updated.setTo(now)
|
||||
)
|
||||
)
|
||||
} yield n
|
||||
}
|
||||
|
||||
def existsByName(coll: Ident, ename: String): ConnectionIO[Boolean] = {
|
||||
val sql = selectCount(eid, table, and(cid.is(coll), name.is(ename)))
|
||||
val t = Table(None)
|
||||
val sql = run(select(count(t.eid)), from(t), where(t.cid === coll, t.name === ename))
|
||||
sql.query[Int].unique.map(_ > 0)
|
||||
}
|
||||
|
||||
def findById(id: Ident): ConnectionIO[Option[REquipment]] = {
|
||||
val sql = selectSimple(all, table, eid.is(id))
|
||||
val t = Table(None)
|
||||
val sql = run(select(t.all), from(t), t.eid === id)
|
||||
sql.query[REquipment].option
|
||||
}
|
||||
|
||||
def findAll(
|
||||
coll: Ident,
|
||||
nameQ: Option[String],
|
||||
order: Columns.type => Column
|
||||
order: Table => Column[_]
|
||||
): ConnectionIO[Vector[REquipment]] = {
|
||||
val q = Seq(cid.is(coll)) ++ (nameQ match {
|
||||
case Some(str) => Seq(name.lowerLike(s"%${str.toLowerCase}%"))
|
||||
case None => Seq.empty
|
||||
})
|
||||
val sql = selectSimple(all, table, and(q)) ++ orderBy(order(Columns).f)
|
||||
val t = Table(None)
|
||||
|
||||
val q = t.cid === coll &&? nameQ
|
||||
.map(str => s"%${str.toLowerCase}%")
|
||||
.map(v => t.name.like(v))
|
||||
|
||||
val sql = Select(select(t.all), from(t), q).orderBy(order(t)).build
|
||||
sql.query[REquipment].to[Vector]
|
||||
}
|
||||
|
||||
def findLike(coll: Ident, equipName: String): ConnectionIO[Vector[IdRef]] =
|
||||
selectSimple(List(eid, name), table, and(cid.is(coll), name.lowerLike(equipName)))
|
||||
def findLike(coll: Ident, equipName: String): ConnectionIO[Vector[IdRef]] = {
|
||||
val t = Table(None)
|
||||
run(select(t.eid, t.name), from(t), t.cid === coll && t.name.like(equipName))
|
||||
.query[IdRef]
|
||||
.to[Vector]
|
||||
|
||||
def delete(id: Ident, coll: Ident): ConnectionIO[Int] =
|
||||
deleteFrom(table, and(eid.is(id), cid.is(coll))).update.run
|
||||
}
|
||||
|
||||
def delete(id: Ident, coll: Ident): ConnectionIO[Int] = {
|
||||
val t = Table(None)
|
||||
DML.delete(t, t.eid === id && t.cid === coll)
|
||||
}
|
||||
}
|
||||
|
@ -1,11 +1,13 @@
|
||||
package docspell.store.records
|
||||
|
||||
import java.time.Instant
|
||||
|
||||
import cats.data.NonEmptyList
|
||||
import cats.implicits._
|
||||
|
||||
import docspell.common._
|
||||
import docspell.store.impl.Implicits._
|
||||
import docspell.store.impl._
|
||||
import docspell.store.qb.DSL._
|
||||
import docspell.store.qb._
|
||||
import docspell.store.syntax.MimeTypes._
|
||||
|
||||
import bitpeace.FileMeta
|
||||
@ -14,26 +16,30 @@ import doobie._
|
||||
import doobie.implicits._
|
||||
|
||||
object RFileMeta {
|
||||
final case class Table(alias: Option[String]) extends TableDef {
|
||||
val tableName = "filemeta"
|
||||
|
||||
val table = fr"filemeta"
|
||||
val id = Column[Ident]("id", this)
|
||||
val timestamp = Column[Instant]("timestamp", this)
|
||||
val mimetype = Column[Mimetype]("mimetype", this)
|
||||
val length = Column[Long]("length", this)
|
||||
val checksum = Column[String]("checksum", this)
|
||||
val chunks = Column[Int]("chunks", this)
|
||||
val chunksize = Column[Int]("chunksize", this)
|
||||
|
||||
object Columns {
|
||||
val id = Column("id")
|
||||
val timestamp = Column("timestamp")
|
||||
val mimetype = Column("mimetype")
|
||||
val length = Column("length")
|
||||
val checksum = Column("checksum")
|
||||
val chunks = Column("chunks")
|
||||
val chunksize = Column("chunksize")
|
||||
|
||||
val all = List(id, timestamp, mimetype, length, checksum, chunks, chunksize)
|
||||
val all = NonEmptyList
|
||||
.of[Column[_]](id, timestamp, mimetype, length, checksum, chunks, chunksize)
|
||||
|
||||
}
|
||||
|
||||
val T = Table(None)
|
||||
def as(alias: String): Table =
|
||||
Table(Some(alias))
|
||||
|
||||
def findById(fid: Ident): ConnectionIO[Option[FileMeta]] = {
|
||||
import bitpeace.sql._
|
||||
|
||||
selectSimple(Columns.all, table, Columns.id.is(fid)).query[FileMeta].option
|
||||
run(select(T.all), from(T), T.id === fid).query[FileMeta].option
|
||||
}
|
||||
|
||||
def findByIds(ids: List[Ident]): ConnectionIO[Vector[FileMeta]] = {
|
||||
@ -41,7 +47,7 @@ object RFileMeta {
|
||||
|
||||
NonEmptyList.fromList(ids) match {
|
||||
case Some(nel) =>
|
||||
selectSimple(Columns.all, table, Columns.id.isIn(nel)).query[FileMeta].to[Vector]
|
||||
run(select(T.all), from(T), T.id.in(nel)).query[FileMeta].to[Vector]
|
||||
case None =>
|
||||
Vector.empty[FileMeta].pure[ConnectionIO]
|
||||
}
|
||||
@ -50,7 +56,7 @@ object RFileMeta {
|
||||
def findMime(fid: Ident): ConnectionIO[Option[MimeType]] = {
|
||||
import bitpeace.sql._
|
||||
|
||||
selectSimple(Seq(Columns.mimetype), table, Columns.id.is(fid))
|
||||
run(select(T.mimetype), from(T), T.id === fid)
|
||||
.query[Mimetype]
|
||||
.option
|
||||
.map(_.map(_.toLocal))
|
||||
|
@ -1,11 +1,12 @@
|
||||
package docspell.store.records
|
||||
|
||||
import cats.data.NonEmptyList
|
||||
import cats.effect._
|
||||
import cats.implicits._
|
||||
|
||||
import docspell.common._
|
||||
import docspell.store.impl.Column
|
||||
import docspell.store.impl.Implicits._
|
||||
import docspell.store.qb.DSL._
|
||||
import docspell.store.qb._
|
||||
|
||||
import doobie._
|
||||
import doobie.implicits._
|
||||
@ -26,61 +27,58 @@ object RFolder {
|
||||
now <- Timestamp.current[F]
|
||||
} yield RFolder(nId, name, account.collective, account.user, now)
|
||||
|
||||
val table = fr"folder"
|
||||
final case class Table(alias: Option[String]) extends TableDef {
|
||||
val tableName = "folder"
|
||||
|
||||
object Columns {
|
||||
val id = Column[Ident]("id", this)
|
||||
val name = Column[String]("name", this)
|
||||
val collective = Column[Ident]("cid", this)
|
||||
val owner = Column[Ident]("owner", this)
|
||||
val created = Column[Timestamp]("created", this)
|
||||
|
||||
val id = Column("id")
|
||||
val name = Column("name")
|
||||
val collective = Column("cid")
|
||||
val owner = Column("owner")
|
||||
val created = Column("created")
|
||||
|
||||
val all = List(id, name, collective, owner, created)
|
||||
val all = NonEmptyList.of[Column[_]](id, name, collective, owner, created)
|
||||
}
|
||||
|
||||
import Columns._
|
||||
val T = Table(None)
|
||||
def as(alias: String): Table =
|
||||
Table(Some(alias))
|
||||
|
||||
def insert(value: RFolder): ConnectionIO[Int] = {
|
||||
val sql = insertRow(
|
||||
table,
|
||||
all,
|
||||
def insert(value: RFolder): ConnectionIO[Int] =
|
||||
DML.insert(
|
||||
T,
|
||||
T.all,
|
||||
fr"${value.id},${value.name},${value.collectiveId},${value.owner},${value.created}"
|
||||
)
|
||||
sql.update.run
|
||||
}
|
||||
|
||||
def update(v: RFolder): ConnectionIO[Int] =
|
||||
updateRow(
|
||||
table,
|
||||
and(id.is(v.id), collective.is(v.collectiveId), owner.is(v.owner)),
|
||||
name.setTo(v.name)
|
||||
).update.run
|
||||
DML.update(
|
||||
T,
|
||||
T.id === v.id && T.collective === v.collectiveId && T.owner === v.owner,
|
||||
DML.set(T.name.setTo(v.name))
|
||||
)
|
||||
|
||||
def existsByName(coll: Ident, folderName: String): ConnectionIO[Boolean] =
|
||||
selectCount(id, table, and(collective.is(coll), name.is(folderName)))
|
||||
run(select(count(T.id)), from(T), T.collective === coll && T.name === folderName)
|
||||
.query[Int]
|
||||
.unique
|
||||
.map(_ > 0)
|
||||
|
||||
def findById(folderId: Ident): ConnectionIO[Option[RFolder]] = {
|
||||
val sql = selectSimple(all, table, id.is(folderId))
|
||||
val sql = run(select(T.all), from(T), T.id === folderId)
|
||||
sql.query[RFolder].option
|
||||
}
|
||||
|
||||
def findAll(
|
||||
coll: Ident,
|
||||
nameQ: Option[String],
|
||||
order: Columns.type => Column
|
||||
order: Table => Column[_]
|
||||
): ConnectionIO[Vector[RFolder]] = {
|
||||
val q = Seq(collective.is(coll)) ++ (nameQ match {
|
||||
case Some(str) => Seq(name.lowerLike(s"%${str.toLowerCase}%"))
|
||||
case None => Seq.empty
|
||||
})
|
||||
val sql = selectSimple(all, table, and(q)) ++ orderBy(order(Columns).f)
|
||||
sql.query[RFolder].to[Vector]
|
||||
val nameFilter = nameQ.map(n => T.name.like(s"%${n.toLowerCase}%"))
|
||||
val sql = Select(select(T.all), from(T), T.collective === coll &&? nameFilter)
|
||||
.orderBy(order(T))
|
||||
sql.build.query[RFolder].to[Vector]
|
||||
}
|
||||
|
||||
def delete(folderId: Ident): ConnectionIO[Int] =
|
||||
deleteFrom(table, id.is(folderId)).update.run
|
||||
DML.delete(T, T.id === folderId)
|
||||
}
|
||||
|
@ -1,11 +1,12 @@
|
||||
package docspell.store.records
|
||||
|
||||
import cats.data.NonEmptyList
|
||||
import cats.effect._
|
||||
import cats.implicits._
|
||||
|
||||
import docspell.common._
|
||||
import docspell.store.impl.Column
|
||||
import docspell.store.impl.Implicits._
|
||||
import docspell.store.qb.DSL._
|
||||
import docspell.store.qb._
|
||||
|
||||
import doobie._
|
||||
import doobie.implicits._
|
||||
@ -25,37 +26,36 @@ object RFolderMember {
|
||||
now <- Timestamp.current[F]
|
||||
} yield RFolderMember(nId, folder, user, now)
|
||||
|
||||
val table = fr"folder_member"
|
||||
final case class Table(alias: Option[String]) extends TableDef {
|
||||
val tableName = "folder_member"
|
||||
|
||||
object Columns {
|
||||
val id = Column[Ident]("id", this)
|
||||
val folder = Column[Ident]("folder_id", this)
|
||||
val user = Column[Ident]("user_id", this)
|
||||
val created = Column[Timestamp]("created", this)
|
||||
|
||||
val id = Column("id")
|
||||
val folder = Column("folder_id")
|
||||
val user = Column("user_id")
|
||||
val created = Column("created")
|
||||
|
||||
val all = List(id, folder, user, created)
|
||||
val all = NonEmptyList.of[Column[_]](id, folder, user, created)
|
||||
}
|
||||
|
||||
import Columns._
|
||||
val T = Table(None)
|
||||
def as(alias: String): Table =
|
||||
Table(Some(alias))
|
||||
|
||||
def insert(value: RFolderMember): ConnectionIO[Int] = {
|
||||
val sql = insertRow(
|
||||
table,
|
||||
all,
|
||||
def insert(value: RFolderMember): ConnectionIO[Int] =
|
||||
DML.insert(
|
||||
T,
|
||||
T.all,
|
||||
fr"${value.id},${value.folderId},${value.userId},${value.created}"
|
||||
)
|
||||
sql.update.run
|
||||
}
|
||||
|
||||
def findByUserId(userId: Ident, folderId: Ident): ConnectionIO[Option[RFolderMember]] =
|
||||
selectSimple(all, table, and(folder.is(folderId), user.is(userId)))
|
||||
run(select(T.all), from(T), T.folder === folderId && T.user === userId)
|
||||
.query[RFolderMember]
|
||||
.option
|
||||
|
||||
def delete(userId: Ident, folderId: Ident): ConnectionIO[Int] =
|
||||
deleteFrom(table, and(folder.is(folderId), user.is(userId))).update.run
|
||||
DML.delete(T, T.folder === folderId && T.user === userId)
|
||||
|
||||
def deleteAll(folderId: Ident): ConnectionIO[Int] =
|
||||
deleteFrom(table, folder.is(folderId)).update.run
|
||||
DML.delete(T, T.folder === folderId)
|
||||
}
|
||||
|
@ -1,11 +1,12 @@
|
||||
package docspell.store.records
|
||||
|
||||
import cats.data.NonEmptyList
|
||||
import cats.effect._
|
||||
import cats.implicits._
|
||||
|
||||
import docspell.common._
|
||||
import docspell.store.impl.Implicits._
|
||||
import docspell.store.impl._
|
||||
import docspell.store.qb.DSL._
|
||||
import docspell.store.qb._
|
||||
|
||||
import doobie._
|
||||
import doobie.implicits._
|
||||
@ -30,32 +31,38 @@ object RFtsMigration {
|
||||
now <- Timestamp.current[F]
|
||||
} yield RFtsMigration(newId, version, ftsEngine, description, now)
|
||||
|
||||
val table = fr"fts_migration"
|
||||
final case class Table(alias: Option[String]) extends TableDef {
|
||||
val tableName = "fts_migration"
|
||||
|
||||
object Columns {
|
||||
val id = Column("id")
|
||||
val version = Column("version")
|
||||
val ftsEngine = Column("fts_engine")
|
||||
val description = Column("description")
|
||||
val created = Column("created")
|
||||
val id = Column[Ident]("id", this)
|
||||
val version = Column[Int]("version", this)
|
||||
val ftsEngine = Column[Ident]("fts_engine", this)
|
||||
val description = Column[String]("description", this)
|
||||
val created = Column[Timestamp]("created", this)
|
||||
|
||||
val all = List(id, version, ftsEngine, description, created)
|
||||
val all = NonEmptyList.of[Column[_]](id, version, ftsEngine, description, created)
|
||||
}
|
||||
import Columns._
|
||||
|
||||
val T = Table(None)
|
||||
def as(alias: String): Table =
|
||||
Table(Some(alias))
|
||||
|
||||
def insert(v: RFtsMigration): ConnectionIO[Int] =
|
||||
insertRow(
|
||||
table,
|
||||
all,
|
||||
fr"${v.id},${v.version},${v.ftsEngine},${v.description},${v.created}"
|
||||
).updateWithLogHandler(LogHandler.nop).run
|
||||
DML
|
||||
.insertFragment(
|
||||
T,
|
||||
T.all,
|
||||
Seq(fr"${v.id},${v.version},${v.ftsEngine},${v.description},${v.created}")
|
||||
)
|
||||
.updateWithLogHandler(LogHandler.nop)
|
||||
.run
|
||||
|
||||
def exists(vers: Int, engine: Ident): ConnectionIO[Boolean] =
|
||||
selectCount(id, table, and(version.is(vers), ftsEngine.is(engine)))
|
||||
run(select(count(T.id)), from(T), T.version === vers && T.ftsEngine === engine)
|
||||
.query[Int]
|
||||
.unique
|
||||
.map(_ > 0)
|
||||
|
||||
def deleteById(rId: Ident): ConnectionIO[Int] =
|
||||
deleteFrom(table, id.is(rId)).update.run
|
||||
DML.delete(T, T.id === rId)
|
||||
}
|
||||
|
@ -1,11 +1,12 @@
|
||||
package docspell.store.records
|
||||
|
||||
import cats.data.NonEmptyList
|
||||
import cats.effect.Sync
|
||||
import cats.implicits._
|
||||
|
||||
import docspell.common._
|
||||
import docspell.store.impl.Implicits._
|
||||
import docspell.store.impl._
|
||||
import docspell.store.qb.DSL._
|
||||
import docspell.store.qb._
|
||||
|
||||
import doobie._
|
||||
import doobie.implicits._
|
||||
@ -13,15 +14,17 @@ import doobie.implicits._
|
||||
case class RInvitation(id: Ident, created: Timestamp) {}
|
||||
|
||||
object RInvitation {
|
||||
final case class Table(alias: Option[String]) extends TableDef {
|
||||
val tableName = "invitation"
|
||||
|
||||
val table = fr"invitation"
|
||||
|
||||
object Columns {
|
||||
val id = Column("id")
|
||||
val created = Column("created")
|
||||
val all = List(id, created)
|
||||
val id = Column[Ident]("id", this)
|
||||
val created = Column[Timestamp]("created", this)
|
||||
val all = NonEmptyList.of[Column[_]](id, created)
|
||||
}
|
||||
import Columns._
|
||||
|
||||
val T = Table(None)
|
||||
def as(alias: String): Table =
|
||||
Table(Some(alias))
|
||||
|
||||
def generate[F[_]: Sync]: F[RInvitation] =
|
||||
for {
|
||||
@ -30,19 +33,19 @@ object RInvitation {
|
||||
} yield RInvitation(i, c)
|
||||
|
||||
def insert(v: RInvitation): ConnectionIO[Int] =
|
||||
insertRow(table, all, fr"${v.id},${v.created}").update.run
|
||||
DML.insert(T, T.all, fr"${v.id},${v.created}")
|
||||
|
||||
def insertNew: ConnectionIO[RInvitation] =
|
||||
generate[ConnectionIO].flatMap(v => insert(v).map(_ => v))
|
||||
|
||||
def findById(invite: Ident): ConnectionIO[Option[RInvitation]] =
|
||||
selectSimple(all, table, id.is(invite)).query[RInvitation].option
|
||||
run(select(T.all), from(T), T.id === invite).query[RInvitation].option
|
||||
|
||||
def delete(invite: Ident): ConnectionIO[Int] =
|
||||
deleteFrom(table, id.is(invite)).update.run
|
||||
DML.delete(T, T.id === invite)
|
||||
|
||||
def useInvite(invite: Ident, minCreated: Timestamp): ConnectionIO[Boolean] = {
|
||||
val get = selectCount(id, table, and(id.is(invite), created.isGt(minCreated)))
|
||||
val get = run(select(count(T.id)), from(T), T.id === invite && T.created > minCreated)
|
||||
.query[Int]
|
||||
.unique
|
||||
for {
|
||||
@ -52,5 +55,5 @@ object RInvitation {
|
||||
}
|
||||
|
||||
def deleteOlderThan(ts: Timestamp): ConnectionIO[Int] =
|
||||
deleteFrom(table, created.isLt(ts)).update.run
|
||||
DML.delete(T, T.created < ts)
|
||||
}
|
||||
|
@ -5,8 +5,8 @@ import cats.effect.Sync
|
||||
import cats.implicits._
|
||||
|
||||
import docspell.common._
|
||||
import docspell.store.impl.Implicits._
|
||||
import docspell.store.impl._
|
||||
import docspell.store.qb.DSL._
|
||||
import docspell.store.qb._
|
||||
|
||||
import doobie._
|
||||
import doobie.implicits._
|
||||
@ -63,27 +63,28 @@ object RItem {
|
||||
None
|
||||
)
|
||||
|
||||
val table = fr"item"
|
||||
final case class Table(alias: Option[String]) extends TableDef {
|
||||
import docspell.store.qb.Column
|
||||
val tableName = "item"
|
||||
|
||||
object Columns {
|
||||
val id = Column("itemid")
|
||||
val cid = Column("cid")
|
||||
val name = Column("name")
|
||||
val itemDate = Column("itemdate")
|
||||
val source = Column("source")
|
||||
val incoming = Column("incoming")
|
||||
val state = Column("state")
|
||||
val corrOrg = Column("corrorg")
|
||||
val corrPerson = Column("corrperson")
|
||||
val concPerson = Column("concperson")
|
||||
val concEquipment = Column("concequipment")
|
||||
val inReplyTo = Column("inreplyto")
|
||||
val dueDate = Column("duedate")
|
||||
val created = Column("created")
|
||||
val updated = Column("updated")
|
||||
val notes = Column("notes")
|
||||
val folder = Column("folder_id")
|
||||
val all = List(
|
||||
val id = Column[Ident]("itemid", this)
|
||||
val cid = Column[Ident]("cid", this)
|
||||
val name = Column[String]("name", this)
|
||||
val itemDate = Column[Timestamp]("itemdate", this)
|
||||
val source = Column[String]("source", this)
|
||||
val incoming = Column[Direction]("incoming", this)
|
||||
val state = Column[ItemState]("state", this)
|
||||
val corrOrg = Column[Ident]("corrorg", this)
|
||||
val corrPerson = Column[Ident]("corrperson", this)
|
||||
val concPerson = Column[Ident]("concperson", this)
|
||||
val concEquipment = Column[Ident]("concequipment", this)
|
||||
val inReplyTo = Column[Ident]("inreplyto", this)
|
||||
val dueDate = Column[Timestamp]("duedate", this)
|
||||
val created = Column[Timestamp]("created", this)
|
||||
val updated = Column[Timestamp]("updated", this)
|
||||
val notes = Column[String]("notes", this)
|
||||
val folder = Column[Ident]("folder_id", this)
|
||||
val all = NonEmptyList.of[Column[_]](
|
||||
id,
|
||||
cid,
|
||||
name,
|
||||
@ -103,19 +104,24 @@ object RItem {
|
||||
folder
|
||||
)
|
||||
}
|
||||
import Columns._
|
||||
val T = Table(None)
|
||||
def as(alias: String): Table =
|
||||
Table(Some(alias))
|
||||
|
||||
private val currentTime =
|
||||
Timestamp.current[ConnectionIO]
|
||||
|
||||
def insert(v: RItem): ConnectionIO[Int] =
|
||||
insertRow(
|
||||
table,
|
||||
all,
|
||||
DML.insert(
|
||||
T,
|
||||
T.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.created},${v.updated},${v.notes},${v.folderId}"
|
||||
).update.run
|
||||
)
|
||||
|
||||
def getCollective(itemId: Ident): ConnectionIO[Option[Ident]] =
|
||||
selectSimple(List(cid), table, id.is(itemId)).query[Ident].option
|
||||
Select(T.cid.s, from(T), T.id === itemId).build.query[Ident].option
|
||||
|
||||
def updateState(
|
||||
itemId: Ident,
|
||||
@ -124,11 +130,11 @@ object RItem {
|
||||
): ConnectionIO[Int] =
|
||||
for {
|
||||
t <- currentTime
|
||||
n <- updateRow(
|
||||
table,
|
||||
and(id.is(itemId), state.isIn(existing)),
|
||||
commas(state.setTo(itemState), updated.setTo(t))
|
||||
).update.run
|
||||
n <- DML.update(
|
||||
T,
|
||||
T.id === itemId && T.state.in(existing),
|
||||
DML.set(T.state.setTo(itemState), T.updated.setTo(t))
|
||||
)
|
||||
} yield n
|
||||
|
||||
def updateStateForCollective(
|
||||
@ -138,11 +144,11 @@ object RItem {
|
||||
): ConnectionIO[Int] =
|
||||
for {
|
||||
t <- currentTime
|
||||
n <- updateRow(
|
||||
table,
|
||||
and(id.isIn(itemIds), cid.is(coll)),
|
||||
commas(state.setTo(itemState), updated.setTo(t))
|
||||
).update.run
|
||||
n <- DML.update(
|
||||
T,
|
||||
T.id.in(itemIds) && T.cid === coll,
|
||||
DML.set(T.state.setTo(itemState), T.updated.setTo(t))
|
||||
)
|
||||
} yield n
|
||||
|
||||
def updateDirection(
|
||||
@ -152,11 +158,11 @@ object RItem {
|
||||
): ConnectionIO[Int] =
|
||||
for {
|
||||
t <- currentTime
|
||||
n <- updateRow(
|
||||
table,
|
||||
and(id.isIn(itemIds), cid.is(coll)),
|
||||
commas(incoming.setTo(dir), updated.setTo(t))
|
||||
).update.run
|
||||
n <- DML.update(
|
||||
T,
|
||||
T.id.in(itemIds) && T.cid === coll,
|
||||
DML.set(T.incoming.setTo(dir), T.updated.setTo(t))
|
||||
)
|
||||
} yield n
|
||||
|
||||
def updateCorrOrg(
|
||||
@ -166,21 +172,21 @@ object RItem {
|
||||
): ConnectionIO[Int] =
|
||||
for {
|
||||
t <- currentTime
|
||||
n <- updateRow(
|
||||
table,
|
||||
and(id.isIn(itemIds), cid.is(coll)),
|
||||
commas(corrOrg.setTo(org), updated.setTo(t))
|
||||
).update.run
|
||||
n <- DML.update(
|
||||
T,
|
||||
T.id.in(itemIds) && T.cid === coll,
|
||||
DML.set(T.corrOrg.setTo(org), T.updated.setTo(t))
|
||||
)
|
||||
} yield n
|
||||
|
||||
def removeCorrOrg(coll: Ident, currentOrg: Ident): ConnectionIO[Int] =
|
||||
for {
|
||||
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 <- DML.update(
|
||||
T,
|
||||
T.cid === coll && T.corrOrg === currentOrg,
|
||||
DML.set(T.corrOrg.setTo(None: Option[Ident]), T.updated.setTo(t))
|
||||
)
|
||||
} yield n
|
||||
|
||||
def updateCorrPerson(
|
||||
@ -190,21 +196,21 @@ object RItem {
|
||||
): ConnectionIO[Int] =
|
||||
for {
|
||||
t <- currentTime
|
||||
n <- updateRow(
|
||||
table,
|
||||
and(id.isIn(itemIds), cid.is(coll)),
|
||||
commas(corrPerson.setTo(person), updated.setTo(t))
|
||||
).update.run
|
||||
n <- DML.update(
|
||||
T,
|
||||
T.id.in(itemIds) && T.cid === coll,
|
||||
DML.set(T.corrPerson.setTo(person), T.updated.setTo(t))
|
||||
)
|
||||
} yield n
|
||||
|
||||
def removeCorrPerson(coll: Ident, currentPerson: Ident): ConnectionIO[Int] =
|
||||
for {
|
||||
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 <- DML.update(
|
||||
T,
|
||||
T.cid === coll && T.corrPerson === currentPerson,
|
||||
DML.set(T.corrPerson.setTo(None: Option[Ident]), T.updated.setTo(t))
|
||||
)
|
||||
} yield n
|
||||
|
||||
def updateConcPerson(
|
||||
@ -214,21 +220,21 @@ object RItem {
|
||||
): ConnectionIO[Int] =
|
||||
for {
|
||||
t <- currentTime
|
||||
n <- updateRow(
|
||||
table,
|
||||
and(id.isIn(itemIds), cid.is(coll)),
|
||||
commas(concPerson.setTo(person), updated.setTo(t))
|
||||
).update.run
|
||||
n <- DML.update(
|
||||
T,
|
||||
T.id.in(itemIds) && T.cid === coll,
|
||||
DML.set(T.concPerson.setTo(person), T.updated.setTo(t))
|
||||
)
|
||||
} yield n
|
||||
|
||||
def removeConcPerson(coll: Ident, currentPerson: Ident): ConnectionIO[Int] =
|
||||
for {
|
||||
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 <- DML.update(
|
||||
T,
|
||||
T.cid === coll && T.concPerson === currentPerson,
|
||||
DML.set(T.concPerson.setTo(None: Option[Ident]), T.updated.setTo(t))
|
||||
)
|
||||
} yield n
|
||||
|
||||
def updateConcEquip(
|
||||
@ -238,21 +244,21 @@ object RItem {
|
||||
): ConnectionIO[Int] =
|
||||
for {
|
||||
t <- currentTime
|
||||
n <- updateRow(
|
||||
table,
|
||||
and(id.isIn(itemIds), cid.is(coll)),
|
||||
commas(concEquipment.setTo(equip), updated.setTo(t))
|
||||
).update.run
|
||||
n <- DML.update(
|
||||
T,
|
||||
T.id.in(itemIds) && T.cid === coll,
|
||||
DML.set(T.concEquipment.setTo(equip), T.updated.setTo(t))
|
||||
)
|
||||
} yield n
|
||||
|
||||
def removeConcEquip(coll: Ident, currentEquip: Ident): ConnectionIO[Int] =
|
||||
for {
|
||||
t <- currentTime
|
||||
n <- updateRow(
|
||||
table,
|
||||
and(cid.is(coll), concEquipment.is(Some(currentEquip))),
|
||||
commas(concEquipment.setTo(None: Option[Ident]), updated.setTo(t))
|
||||
).update.run
|
||||
n <- DML.update(
|
||||
T,
|
||||
T.cid === coll && T.concEquipment === currentEquip,
|
||||
DML.set(T.concEquipment.setTo(None: Option[Ident]), T.updated.setTo(t))
|
||||
)
|
||||
} yield n
|
||||
|
||||
def updateFolder(
|
||||
@ -262,31 +268,31 @@ object RItem {
|
||||
): ConnectionIO[Int] =
|
||||
for {
|
||||
t <- currentTime
|
||||
n <- updateRow(
|
||||
table,
|
||||
and(cid.is(coll), id.is(itemId)),
|
||||
commas(folder.setTo(folderId), updated.setTo(t))
|
||||
).update.run
|
||||
n <- DML.update(
|
||||
T,
|
||||
T.cid === coll && T.id === itemId,
|
||||
DML.set(T.folder.setTo(folderId), T.updated.setTo(t))
|
||||
)
|
||||
} yield n
|
||||
|
||||
def updateNotes(itemId: Ident, coll: Ident, text: Option[String]): ConnectionIO[Int] =
|
||||
for {
|
||||
t <- currentTime
|
||||
n <- updateRow(
|
||||
table,
|
||||
and(id.is(itemId), cid.is(coll)),
|
||||
commas(notes.setTo(text), updated.setTo(t))
|
||||
).update.run
|
||||
n <- DML.update(
|
||||
T,
|
||||
T.id === itemId && T.cid === coll,
|
||||
DML.set(T.notes.setTo(text), T.updated.setTo(t))
|
||||
)
|
||||
} yield n
|
||||
|
||||
def updateName(itemId: Ident, coll: Ident, itemName: String): ConnectionIO[Int] =
|
||||
for {
|
||||
t <- currentTime
|
||||
n <- updateRow(
|
||||
table,
|
||||
and(id.is(itemId), cid.is(coll)),
|
||||
commas(name.setTo(itemName), updated.setTo(t))
|
||||
).update.run
|
||||
n <- DML.update(
|
||||
T,
|
||||
T.id === itemId && T.cid === coll,
|
||||
DML.set(T.name.setTo(itemName), T.updated.setTo(t))
|
||||
)
|
||||
} yield n
|
||||
|
||||
def updateDate(
|
||||
@ -296,11 +302,11 @@ object RItem {
|
||||
): ConnectionIO[Int] =
|
||||
for {
|
||||
t <- currentTime
|
||||
n <- updateRow(
|
||||
table,
|
||||
and(id.isIn(itemIds), cid.is(coll)),
|
||||
commas(itemDate.setTo(date), updated.setTo(t))
|
||||
).update.run
|
||||
n <- DML.update(
|
||||
T,
|
||||
T.id.in(itemIds) && T.cid === coll,
|
||||
DML.set(T.itemDate.setTo(date), T.updated.setTo(t))
|
||||
)
|
||||
} yield n
|
||||
|
||||
def updateDueDate(
|
||||
@ -310,48 +316,51 @@ object RItem {
|
||||
): ConnectionIO[Int] =
|
||||
for {
|
||||
t <- currentTime
|
||||
n <- updateRow(
|
||||
table,
|
||||
and(id.isIn(itemIds), cid.is(coll)),
|
||||
commas(dueDate.setTo(date), updated.setTo(t))
|
||||
).update.run
|
||||
n <- DML.update(
|
||||
T,
|
||||
T.id.in(itemIds) && T.cid === coll,
|
||||
DML.set(T.dueDate.setTo(date), T.updated.setTo(t))
|
||||
)
|
||||
} yield n
|
||||
|
||||
def deleteByIdAndCollective(itemId: Ident, coll: Ident): ConnectionIO[Int] =
|
||||
deleteFrom(table, and(id.is(itemId), cid.is(coll))).update.run
|
||||
DML.delete(T, T.id === itemId && T.cid === coll)
|
||||
|
||||
def existsById(itemId: Ident): ConnectionIO[Boolean] =
|
||||
selectCount(id, table, id.is(itemId)).query[Int].unique.map(_ > 0)
|
||||
Select(count(T.id).s, from(T), T.id === itemId).build.query[Int].unique.map(_ > 0)
|
||||
|
||||
def existsByIdAndCollective(itemId: Ident, coll: Ident): ConnectionIO[Boolean] =
|
||||
selectCount(id, table, and(id.is(itemId), cid.is(coll))).query[Int].unique.map(_ > 0)
|
||||
Select(count(T.id).s, from(T), T.id === itemId && T.cid === coll).build
|
||||
.query[Int]
|
||||
.unique
|
||||
.map(_ > 0)
|
||||
|
||||
def existsByIdsAndCollective(
|
||||
itemIds: NonEmptyList[Ident],
|
||||
coll: Ident
|
||||
): ConnectionIO[Boolean] =
|
||||
selectCount(id, table, and(id.isIn(itemIds), cid.is(coll)))
|
||||
Select(count(T.id).s, from(T), T.id.in(itemIds) && T.cid === coll).build
|
||||
.query[Int]
|
||||
.unique
|
||||
.map(_ == itemIds.size)
|
||||
|
||||
def findByIdAndCollective(itemId: Ident, coll: Ident): ConnectionIO[Option[RItem]] =
|
||||
selectSimple(all, table, and(id.is(itemId), cid.is(coll))).query[RItem].option
|
||||
run(select(T.all), from(T), T.id === itemId && T.cid === coll).query[RItem].option
|
||||
|
||||
def findById(itemId: Ident): ConnectionIO[Option[RItem]] =
|
||||
selectSimple(all, table, id.is(itemId)).query[RItem].option
|
||||
run(select(T.all), from(T), T.id === itemId).query[RItem].option
|
||||
|
||||
def checkByIdAndCollective(itemId: Ident, coll: Ident): ConnectionIO[Option[Ident]] =
|
||||
selectSimple(Seq(id), table, and(id.is(itemId), cid.is(coll))).query[Ident].option
|
||||
Select(T.id.s, from(T), T.id === itemId && T.cid === coll).build.query[Ident].option
|
||||
|
||||
def removeFolder(folderId: Ident): ConnectionIO[Int] = {
|
||||
val empty: Option[Ident] = None
|
||||
updateRow(table, folder.is(folderId), folder.setTo(empty)).update.run
|
||||
DML.update(T, T.folder === folderId, DML.set(T.folder.setTo(empty)))
|
||||
}
|
||||
|
||||
def filterItemsFragment(items: NonEmptyList[Ident], coll: Ident): Fragment =
|
||||
selectSimple(Seq(id), table, and(cid.is(coll), id.isIn(items)))
|
||||
def filterItemsFragment(items: NonEmptyList[Ident], coll: Ident): Select =
|
||||
Select(select(T.id), from(T), T.cid === coll && T.id.in(items))
|
||||
|
||||
def filterItems(items: NonEmptyList[Ident], coll: Ident): ConnectionIO[Vector[Ident]] =
|
||||
filterItemsFragment(items, coll).query[Ident].to[Vector]
|
||||
filterItemsFragment(items, coll).build.query[Ident].to[Vector]
|
||||
}
|
||||
|
@ -1,12 +1,12 @@
|
||||
package docspell.store.records
|
||||
|
||||
import cats.effect.Sync
|
||||
import cats.data.NonEmptyList
|
||||
import cats.implicits._
|
||||
import fs2.Stream
|
||||
|
||||
import docspell.common._
|
||||
import docspell.store.impl.Column
|
||||
import docspell.store.impl.Implicits._
|
||||
import docspell.store.qb.DSL._
|
||||
import docspell.store.qb._
|
||||
|
||||
import doobie._
|
||||
import doobie.implicits._
|
||||
@ -34,7 +34,7 @@ case class RJob(
|
||||
s"${id.id.substring(0, 9)}.../${group.id}/${task.id}/$priority"
|
||||
|
||||
def isFinalState: Boolean =
|
||||
JobState.done.contains(state)
|
||||
JobState.done.toList.contains(state)
|
||||
|
||||
def isInProgress: Boolean =
|
||||
JobState.inProgress.contains(state)
|
||||
@ -71,26 +71,26 @@ object RJob {
|
||||
None
|
||||
)
|
||||
|
||||
val table = fr"job"
|
||||
final case class Table(alias: Option[String]) extends TableDef {
|
||||
val tableName = "job"
|
||||
|
||||
object Columns {
|
||||
val id = Column("jid")
|
||||
val task = Column("task")
|
||||
val group = Column("group_")
|
||||
val args = Column("args")
|
||||
val subject = Column("subject")
|
||||
val submitted = Column("submitted")
|
||||
val submitter = Column("submitter")
|
||||
val priority = Column("priority")
|
||||
val state = Column("state")
|
||||
val retries = Column("retries")
|
||||
val progress = Column("progress")
|
||||
val tracker = Column("tracker")
|
||||
val worker = Column("worker")
|
||||
val started = Column("started")
|
||||
val startedmillis = Column("startedmillis")
|
||||
val finished = Column("finished")
|
||||
val all = List(
|
||||
val id = Column[Ident]("jid", this)
|
||||
val task = Column[Ident]("task", this)
|
||||
val group = Column[Ident]("group_", this)
|
||||
val args = Column[String]("args", this)
|
||||
val subject = Column[String]("subject", this)
|
||||
val submitted = Column[Timestamp]("submitted", this)
|
||||
val submitter = Column[Ident]("submitter", this)
|
||||
val priority = Column[Priority]("priority", this)
|
||||
val state = Column[JobState]("state", this)
|
||||
val retries = Column[Int]("retries", this)
|
||||
val progress = Column[Int]("progress", this)
|
||||
val tracker = Column[Ident]("tracker", this)
|
||||
val worker = Column[Ident]("worker", this)
|
||||
val started = Column[Timestamp]("started", this)
|
||||
val startedmillis = Column[Long]("startedmillis", this)
|
||||
val finished = Column[Timestamp]("finished", this)
|
||||
val all = NonEmptyList.of[Column[_]](
|
||||
id,
|
||||
task,
|
||||
group,
|
||||
@ -109,163 +109,174 @@ object RJob {
|
||||
)
|
||||
}
|
||||
|
||||
import Columns._
|
||||
val T = Table(None)
|
||||
def as(alias: String): Table =
|
||||
Table(Some(alias))
|
||||
|
||||
def insert(v: RJob): ConnectionIO[Int] = {
|
||||
val smillis = v.started.map(_.toMillis)
|
||||
val sql = insertRow(
|
||||
table,
|
||||
all ++ List(startedmillis),
|
||||
DML.insert(
|
||||
T,
|
||||
T.all ++ List(T.startedmillis),
|
||||
fr"${v.id},${v.task},${v.group},${v.args},${v.subject},${v.submitted},${v.submitter},${v.priority},${v.state},${v.retries},${v.progress},${v.tracker},${v.worker},${v.started},${v.finished},$smillis"
|
||||
)
|
||||
sql.update.run
|
||||
}
|
||||
|
||||
def findFromIds(ids: Seq[Ident]): ConnectionIO[Vector[RJob]] =
|
||||
if (ids.isEmpty) Sync[ConnectionIO].pure(Vector.empty[RJob])
|
||||
else selectSimple(all, table, id.isOneOf(ids)).query[RJob].to[Vector]
|
||||
NonEmptyList.fromList(ids.toList) match {
|
||||
case None =>
|
||||
Vector.empty[RJob].pure[ConnectionIO]
|
||||
case Some(nel) =>
|
||||
run(select(T.all), from(T), T.id.in(nel)).query[RJob].to[Vector]
|
||||
}
|
||||
|
||||
def findByIdAndGroup(jobId: Ident, jobGroup: Ident): ConnectionIO[Option[RJob]] =
|
||||
selectSimple(all, table, and(id.is(jobId), group.is(jobGroup))).query[RJob].option
|
||||
run(select(T.all), from(T), T.id === jobId && T.group === jobGroup).query[RJob].option
|
||||
|
||||
def findById(jobId: Ident): ConnectionIO[Option[RJob]] =
|
||||
selectSimple(all, table, id.is(jobId)).query[RJob].option
|
||||
run(select(T.all), from(T), T.id === jobId).query[RJob].option
|
||||
|
||||
def findByIdAndWorker(jobId: Ident, workerId: Ident): ConnectionIO[Option[RJob]] =
|
||||
selectSimple(all, table, and(id.is(jobId), worker.is(workerId))).query[RJob].option
|
||||
run(select(T.all), from(T), T.id === jobId && T.worker === workerId)
|
||||
.query[RJob]
|
||||
.option
|
||||
|
||||
def setRunningToWaiting(workerId: Ident): ConnectionIO[Int] = {
|
||||
val states: Seq[JobState] = List(JobState.Running, JobState.Scheduled)
|
||||
updateRow(
|
||||
table,
|
||||
and(worker.is(workerId), state.isOneOf(states)),
|
||||
state.setTo(JobState.Waiting: JobState)
|
||||
).update.run
|
||||
val states: NonEmptyList[JobState] =
|
||||
NonEmptyList.of(JobState.Running, JobState.Scheduled)
|
||||
DML.update(
|
||||
T,
|
||||
where(T.worker === workerId, T.state.in(states)),
|
||||
DML.set(T.state.setTo(JobState.waiting))
|
||||
)
|
||||
}
|
||||
|
||||
def incrementRetries(jobid: Ident): ConnectionIO[Int] =
|
||||
updateRow(
|
||||
table,
|
||||
and(id.is(jobid), state.is(JobState.Stuck: JobState)),
|
||||
retries.f ++ fr"=" ++ retries.f ++ fr"+ 1"
|
||||
).update.run
|
||||
DML
|
||||
.update(
|
||||
T,
|
||||
where(T.id === jobid, T.state === JobState.stuck),
|
||||
DML.set(T.retries.increment(1))
|
||||
)
|
||||
|
||||
def setRunning(jobId: Ident, workerId: Ident, now: Timestamp): ConnectionIO[Int] =
|
||||
updateRow(
|
||||
table,
|
||||
id.is(jobId),
|
||||
commas(
|
||||
state.setTo(JobState.Running: JobState),
|
||||
started.setTo(now),
|
||||
startedmillis.setTo(now.toMillis),
|
||||
worker.setTo(workerId)
|
||||
DML.update(
|
||||
T,
|
||||
T.id === jobId,
|
||||
DML.set(
|
||||
T.state.setTo(JobState.running),
|
||||
T.started.setTo(now),
|
||||
T.startedmillis.setTo(now.toMillis),
|
||||
T.worker.setTo(workerId)
|
||||
)
|
||||
)
|
||||
).update.run
|
||||
|
||||
def setWaiting(jobId: Ident): ConnectionIO[Int] =
|
||||
updateRow(
|
||||
table,
|
||||
id.is(jobId),
|
||||
commas(
|
||||
state.setTo(JobState.Waiting: JobState),
|
||||
started.setTo(None: Option[Timestamp]),
|
||||
startedmillis.setTo(None: Option[Long]),
|
||||
finished.setTo(None: Option[Timestamp])
|
||||
DML
|
||||
.update(
|
||||
T,
|
||||
T.id === jobId,
|
||||
DML.set(
|
||||
T.state.setTo(JobState.Waiting: JobState),
|
||||
T.started.setTo(None: Option[Timestamp]),
|
||||
T.startedmillis.setTo(None: Option[Long]),
|
||||
T.finished.setTo(None: Option[Timestamp])
|
||||
)
|
||||
)
|
||||
).update.run
|
||||
|
||||
def setScheduled(jobId: Ident, workerId: Ident): ConnectionIO[Int] =
|
||||
for {
|
||||
_ <- incrementRetries(jobId)
|
||||
n <- updateRow(
|
||||
table,
|
||||
and(
|
||||
id.is(jobId),
|
||||
or(worker.isNull, worker.is(workerId)),
|
||||
state.isOneOf(Seq[JobState](JobState.Waiting, JobState.Stuck))
|
||||
n <- DML.update(
|
||||
T,
|
||||
where(
|
||||
T.id === jobId,
|
||||
or(T.worker.isNull, T.worker === workerId),
|
||||
T.state.in(NonEmptyList.of(JobState.waiting, JobState.stuck))
|
||||
),
|
||||
commas(
|
||||
state.setTo(JobState.Scheduled: JobState),
|
||||
worker.setTo(workerId)
|
||||
DML.set(
|
||||
T.state.setTo(JobState.scheduled),
|
||||
T.worker.setTo(workerId)
|
||||
)
|
||||
)
|
||||
).update.run
|
||||
} yield n
|
||||
|
||||
def setSuccess(jobId: Ident, now: Timestamp): ConnectionIO[Int] =
|
||||
updateRow(
|
||||
table,
|
||||
id.is(jobId),
|
||||
commas(
|
||||
state.setTo(JobState.Success: JobState),
|
||||
finished.setTo(now)
|
||||
DML
|
||||
.update(
|
||||
T,
|
||||
T.id === jobId,
|
||||
DML.set(
|
||||
T.state.setTo(JobState.success),
|
||||
T.finished.setTo(now)
|
||||
)
|
||||
)
|
||||
).update.run
|
||||
|
||||
def setStuck(jobId: Ident, now: Timestamp): ConnectionIO[Int] =
|
||||
updateRow(
|
||||
table,
|
||||
id.is(jobId),
|
||||
commas(
|
||||
state.setTo(JobState.Stuck: JobState),
|
||||
finished.setTo(now)
|
||||
DML.update(
|
||||
T,
|
||||
T.id === jobId,
|
||||
DML.set(
|
||||
T.state.setTo(JobState.stuck),
|
||||
T.finished.setTo(now)
|
||||
)
|
||||
)
|
||||
).update.run
|
||||
|
||||
def setFailed(jobId: Ident, now: Timestamp): ConnectionIO[Int] =
|
||||
updateRow(
|
||||
table,
|
||||
id.is(jobId),
|
||||
commas(
|
||||
state.setTo(JobState.Failed: JobState),
|
||||
finished.setTo(now)
|
||||
DML.update(
|
||||
T,
|
||||
T.id === jobId,
|
||||
DML.set(
|
||||
T.state.setTo(JobState.failed),
|
||||
T.finished.setTo(now)
|
||||
)
|
||||
)
|
||||
).update.run
|
||||
|
||||
def setCancelled(jobId: Ident, now: Timestamp): ConnectionIO[Int] =
|
||||
updateRow(
|
||||
table,
|
||||
id.is(jobId),
|
||||
commas(
|
||||
state.setTo(JobState.Cancelled: JobState),
|
||||
finished.setTo(now)
|
||||
DML.update(
|
||||
T,
|
||||
T.id === jobId,
|
||||
DML.set(
|
||||
T.state.setTo(JobState.cancelled),
|
||||
T.finished.setTo(now)
|
||||
)
|
||||
)
|
||||
).update.run
|
||||
|
||||
def setPriority(jobId: Ident, jobGroup: Ident, prio: Priority): ConnectionIO[Int] =
|
||||
updateRow(
|
||||
table,
|
||||
and(id.is(jobId), group.is(jobGroup), state.is(JobState.waiting)),
|
||||
priority.setTo(prio)
|
||||
).update.run
|
||||
DML.update(
|
||||
T,
|
||||
where(T.id === jobId, T.group === jobGroup, T.state === JobState.waiting),
|
||||
DML.set(T.priority.setTo(prio))
|
||||
)
|
||||
|
||||
def getRetries(jobId: Ident): ConnectionIO[Option[Int]] =
|
||||
selectSimple(List(retries), table, id.is(jobId)).query[Int].option
|
||||
run(select(T.retries), from(T), T.id === jobId).query[Int].option
|
||||
|
||||
def setProgress(jobId: Ident, perc: Int): ConnectionIO[Int] =
|
||||
updateRow(table, id.is(jobId), progress.setTo(perc)).update.run
|
||||
DML.update(T, T.id === jobId, DML.set(T.progress.setTo(perc)))
|
||||
|
||||
def selectWaiting: ConnectionIO[Option[RJob]] = {
|
||||
val sql = selectSimple(all, table, state.is(JobState.Waiting: JobState))
|
||||
val sql = run(select(T.all), from(T), T.state === JobState.waiting)
|
||||
sql.query[RJob].to[Vector].map(_.headOption)
|
||||
}
|
||||
|
||||
def selectGroupInState(states: Seq[JobState]): ConnectionIO[Vector[Ident]] = {
|
||||
def selectGroupInState(states: NonEmptyList[JobState]): ConnectionIO[Vector[Ident]] = {
|
||||
val sql =
|
||||
selectDistinct(List(group), table, state.isOneOf(states)) ++ orderBy(group.f)
|
||||
sql.query[Ident].to[Vector]
|
||||
Select(select(T.group), from(T), T.state.in(states)).orderBy(T.group)
|
||||
sql.build.query[Ident].to[Vector]
|
||||
}
|
||||
|
||||
def delete(jobId: Ident): ConnectionIO[Int] =
|
||||
for {
|
||||
n0 <- RJobLog.deleteAll(jobId)
|
||||
n1 <- deleteFrom(table, id.is(jobId)).update.run
|
||||
n1 <- DML.delete(T, T.id === jobId)
|
||||
} yield n0 + n1
|
||||
|
||||
def findIdsDoneAndOlderThan(ts: Timestamp): Stream[ConnectionIO, Ident] =
|
||||
selectSimple(
|
||||
Seq(id),
|
||||
table,
|
||||
and(state.isOneOf(JobState.done.toSeq), or(finished.isNull, finished.isLt(ts)))
|
||||
run(
|
||||
select(T.id),
|
||||
from(T),
|
||||
T.state.in(JobState.done) && (T.finished.isNull || T.finished < ts)
|
||||
).query[Ident].stream
|
||||
|
||||
def deleteDoneAndOlderThan(ts: Timestamp, batch: Int): ConnectionIO[Int] =
|
||||
@ -277,10 +288,10 @@ object RJob {
|
||||
.foldMonoid
|
||||
|
||||
def findNonFinalByTracker(trackerId: Ident): ConnectionIO[Option[RJob]] =
|
||||
selectSimple(
|
||||
all,
|
||||
table,
|
||||
and(tracker.is(trackerId), state.isOneOf(JobState.all.diff(JobState.done).toSeq))
|
||||
run(
|
||||
select(T.all),
|
||||
from(T),
|
||||
where(T.tracker === trackerId, T.state.in(JobState.notDone))
|
||||
).query[RJob].option
|
||||
|
||||
}
|
||||
|
@ -1,10 +1,11 @@
|
||||
package docspell.store.records
|
||||
|
||||
import cats.data.NonEmptyList
|
||||
import cats.implicits._
|
||||
|
||||
import docspell.common._
|
||||
import docspell.store.impl.Column
|
||||
import docspell.store.impl.Implicits._
|
||||
import docspell.store.qb.DSL._
|
||||
import docspell.store.qb._
|
||||
|
||||
import doobie._
|
||||
import doobie.implicits._
|
||||
@ -12,25 +13,27 @@ import doobie.implicits._
|
||||
case class RJobGroupUse(groupId: Ident, workerId: Ident) {}
|
||||
|
||||
object RJobGroupUse {
|
||||
final case class Table(alias: Option[String]) extends TableDef {
|
||||
val tableName = "jobgroupuse"
|
||||
|
||||
val table = fr"jobgroupuse"
|
||||
|
||||
object Columns {
|
||||
val group = Column("groupid")
|
||||
val worker = Column("workerid")
|
||||
val all = List(group, worker)
|
||||
val group = Column[Ident]("groupid", this)
|
||||
val worker = Column[Ident]("workerid", this)
|
||||
val all = NonEmptyList.of[Column[_]](group, worker)
|
||||
}
|
||||
import Columns._
|
||||
|
||||
val T = Table(None)
|
||||
def as(alias: String): Table =
|
||||
Table(Some(alias))
|
||||
|
||||
def insert(v: RJobGroupUse): ConnectionIO[Int] =
|
||||
insertRow(table, all, fr"${v.groupId},${v.workerId}").update.run
|
||||
DML.insert(T, T.all, fr"${v.groupId},${v.workerId}")
|
||||
|
||||
def updateGroup(v: RJobGroupUse): ConnectionIO[Int] =
|
||||
updateRow(table, worker.is(v.workerId), group.setTo(v.groupId)).update.run
|
||||
DML.update(T, T.worker === v.workerId, DML.set(T.group.setTo(v.groupId)))
|
||||
|
||||
def setGroup(v: RJobGroupUse): ConnectionIO[Int] =
|
||||
updateGroup(v).flatMap(n => if (n > 0) n.pure[ConnectionIO] else insert(v))
|
||||
|
||||
def findGroup(workerId: Ident): ConnectionIO[Option[Ident]] =
|
||||
selectSimple(List(group), table, worker.is(workerId)).query[Ident].option
|
||||
run(select(T.group), from(T), T.worker === workerId).query[Ident].option
|
||||
}
|
||||
|
@ -1,8 +1,10 @@
|
||||
package docspell.store.records
|
||||
|
||||
import cats.data.NonEmptyList
|
||||
|
||||
import docspell.common._
|
||||
import docspell.store.impl.Column
|
||||
import docspell.store.impl.Implicits._
|
||||
import docspell.store.qb.DSL._
|
||||
import docspell.store.qb._
|
||||
|
||||
import doobie._
|
||||
import doobie.implicits._
|
||||
@ -16,35 +18,39 @@ case class RJobLog(
|
||||
) {}
|
||||
|
||||
object RJobLog {
|
||||
final case class Table(alias: Option[String]) extends TableDef {
|
||||
val tableName = "joblog"
|
||||
|
||||
val table = fr"joblog"
|
||||
|
||||
object Columns {
|
||||
val id = Column("id")
|
||||
val jobId = Column("jid")
|
||||
val level = Column("level")
|
||||
val created = Column("created")
|
||||
val message = Column("message")
|
||||
val all = List(id, jobId, level, created, message)
|
||||
val id = Column[Ident]("id", this)
|
||||
val jobId = Column[Ident]("jid", this)
|
||||
val level = Column[LogLevel]("level", this)
|
||||
val created = Column[Timestamp]("created", this)
|
||||
val message = Column[String]("message", this)
|
||||
val all = NonEmptyList.of[Column[_]](id, jobId, level, created, message)
|
||||
|
||||
// separate column only for sorting, so not included in `all` and
|
||||
// the case class
|
||||
val counter = Column("counter")
|
||||
val counter = Column[Long]("counter", this)
|
||||
}
|
||||
import Columns._
|
||||
|
||||
val T = Table(None)
|
||||
def as(alias: String): Table =
|
||||
Table(Some(alias))
|
||||
|
||||
def insert(v: RJobLog): ConnectionIO[Int] =
|
||||
insertRow(
|
||||
table,
|
||||
all,
|
||||
DML.insert(
|
||||
T,
|
||||
T.all,
|
||||
fr"${v.id},${v.jobId},${v.level},${v.created},${v.message}"
|
||||
).update.run
|
||||
)
|
||||
|
||||
def findLogs(id: Ident): ConnectionIO[Vector[RJobLog]] =
|
||||
(selectSimple(all, table, jobId.is(id)) ++ orderBy(created.asc, counter.asc))
|
||||
Select(select(T.all), from(T), T.jobId === id)
|
||||
.orderBy(T.created.asc, T.counter.asc)
|
||||
.build
|
||||
.query[RJobLog]
|
||||
.to[Vector]
|
||||
|
||||
def deleteAll(job: Ident): ConnectionIO[Int] =
|
||||
deleteFrom(table, jobId.is(job)).update.run
|
||||
DML.delete(T, T.jobId === job)
|
||||
}
|
||||
|
@ -1,11 +1,12 @@
|
||||
package docspell.store.records
|
||||
|
||||
import cats.data.NonEmptyList
|
||||
import cats.effect.Sync
|
||||
import cats.implicits._
|
||||
|
||||
import docspell.common._
|
||||
import docspell.store.impl.Column
|
||||
import docspell.store.impl.Implicits._
|
||||
import docspell.store.qb.DSL._
|
||||
import docspell.store.qb._
|
||||
|
||||
import doobie._
|
||||
import doobie.implicits._
|
||||
@ -23,35 +24,42 @@ object RNode {
|
||||
def apply[F[_]: Sync](id: Ident, nodeType: NodeType, uri: LenientUri): F[RNode] =
|
||||
Timestamp.current[F].map(now => RNode(id, nodeType, uri, now, now))
|
||||
|
||||
val table = fr"node"
|
||||
final case class Table(alias: Option[String]) extends TableDef {
|
||||
val tableName = "node"
|
||||
|
||||
object Columns {
|
||||
val id = Column("id")
|
||||
val nodeType = Column("type")
|
||||
val url = Column("url")
|
||||
val updated = Column("updated")
|
||||
val created = Column("created")
|
||||
val all = List(id, nodeType, url, updated, created)
|
||||
val id = Column[Ident]("id", this)
|
||||
val nodeType = Column[NodeType]("type", this)
|
||||
val url = Column[LenientUri]("url", this)
|
||||
val updated = Column[Timestamp]("updated", this)
|
||||
val created = Column[Timestamp]("created", this)
|
||||
val all = NonEmptyList.of[Column[_]](id, nodeType, url, updated, created)
|
||||
}
|
||||
import Columns._
|
||||
|
||||
def insert(v: RNode): ConnectionIO[Int] =
|
||||
insertRow(
|
||||
table,
|
||||
all,
|
||||
def as(alias: String): Table =
|
||||
Table(Some(alias))
|
||||
|
||||
def insert(v: RNode): ConnectionIO[Int] = {
|
||||
val t = Table(None)
|
||||
DML.insert(
|
||||
t,
|
||||
t.all,
|
||||
fr"${v.id},${v.nodeType},${v.url},${v.updated},${v.created}"
|
||||
).update.run
|
||||
|
||||
def update(v: RNode): ConnectionIO[Int] =
|
||||
updateRow(
|
||||
table,
|
||||
id.is(v.id),
|
||||
commas(
|
||||
nodeType.setTo(v.nodeType),
|
||||
url.setTo(v.url),
|
||||
updated.setTo(v.updated)
|
||||
)
|
||||
).update.run
|
||||
}
|
||||
|
||||
def update(v: RNode): ConnectionIO[Int] = {
|
||||
val t = Table(None)
|
||||
DML
|
||||
.update(
|
||||
t,
|
||||
t.id === v.id,
|
||||
DML.set(
|
||||
t.nodeType.setTo(v.nodeType),
|
||||
t.url.setTo(v.url),
|
||||
t.updated.setTo(v.updated)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
def set(v: RNode): ConnectionIO[Int] =
|
||||
for {
|
||||
@ -59,12 +67,18 @@ object RNode {
|
||||
k <- if (n == 0) insert(v) else 0.pure[ConnectionIO]
|
||||
} yield n + k
|
||||
|
||||
def delete(appId: Ident): ConnectionIO[Int] =
|
||||
(fr"DELETE FROM" ++ table ++ where(id.is(appId))).update.run
|
||||
|
||||
def findAll(nt: NodeType): ConnectionIO[Vector[RNode]] =
|
||||
selectSimple(all, table, nodeType.is(nt)).query[RNode].to[Vector]
|
||||
|
||||
def findById(nodeId: Ident): ConnectionIO[Option[RNode]] =
|
||||
selectSimple(all, table, id.is(nodeId)).query[RNode].option
|
||||
def delete(appId: Ident): ConnectionIO[Int] = {
|
||||
val t = Table(None)
|
||||
DML.delete(t, t.id === appId)
|
||||
}
|
||||
|
||||
def findAll(nt: NodeType): ConnectionIO[Vector[RNode]] = {
|
||||
val t = Table(None)
|
||||
run(select(t.all), from(t), t.nodeType === nt).query[RNode].to[Vector]
|
||||
}
|
||||
|
||||
def findById(nodeId: Ident): ConnectionIO[Option[RNode]] = {
|
||||
val t = Table(None)
|
||||
run(select(t.all), from(t), t.id === nodeId).query[RNode].option
|
||||
}
|
||||
}
|
||||
|
@ -1,11 +1,12 @@
|
||||
package docspell.store.records
|
||||
|
||||
import cats.Eq
|
||||
import cats.data.NonEmptyList
|
||||
import fs2.Stream
|
||||
|
||||
import docspell.common.{IdRef, _}
|
||||
import docspell.store.impl.Implicits._
|
||||
import docspell.store.impl._
|
||||
import docspell.store.qb.DSL._
|
||||
import docspell.store.qb._
|
||||
|
||||
import doobie._
|
||||
import doobie.implicits._
|
||||
@ -27,73 +28,85 @@ object ROrganization {
|
||||
implicit val orgEq: Eq[ROrganization] =
|
||||
Eq.by[ROrganization, Ident](_.oid)
|
||||
|
||||
val table = fr"organization"
|
||||
final case class Table(alias: Option[String]) extends TableDef {
|
||||
val tableName = "organization"
|
||||
|
||||
object Columns {
|
||||
val oid = Column("oid")
|
||||
val cid = Column("cid")
|
||||
val name = Column("name")
|
||||
val street = Column("street")
|
||||
val zip = Column("zip")
|
||||
val city = Column("city")
|
||||
val country = Column("country")
|
||||
val notes = Column("notes")
|
||||
val created = Column("created")
|
||||
val updated = Column("updated")
|
||||
val all = List(oid, cid, name, street, zip, city, country, notes, created, updated)
|
||||
val oid = Column[Ident]("oid", this)
|
||||
val cid = Column[Ident]("cid", this)
|
||||
val name = Column[String]("name", this)
|
||||
val street = Column[String]("street", this)
|
||||
val zip = Column[String]("zip", this)
|
||||
val city = Column[String]("city", this)
|
||||
val country = Column[String]("country", this)
|
||||
val notes = Column[String]("notes", this)
|
||||
val created = Column[Timestamp]("created", this)
|
||||
val updated = Column[Timestamp]("updated", this)
|
||||
val all =
|
||||
NonEmptyList.of[Column[_]](
|
||||
oid,
|
||||
cid,
|
||||
name,
|
||||
street,
|
||||
zip,
|
||||
city,
|
||||
country,
|
||||
notes,
|
||||
created,
|
||||
updated
|
||||
)
|
||||
}
|
||||
|
||||
import Columns._
|
||||
val T = Table(None)
|
||||
def as(alias: String): Table =
|
||||
Table(Some(alias))
|
||||
|
||||
def insert(v: ROrganization): ConnectionIO[Int] = {
|
||||
val sql = insertRow(
|
||||
table,
|
||||
all,
|
||||
def insert(v: ROrganization): ConnectionIO[Int] =
|
||||
DML.insert(
|
||||
T,
|
||||
T.all,
|
||||
fr"${v.oid},${v.cid},${v.name},${v.street},${v.zip},${v.city},${v.country},${v.notes},${v.created},${v.updated}"
|
||||
)
|
||||
sql.update.run
|
||||
}
|
||||
|
||||
def update(v: ROrganization): ConnectionIO[Int] = {
|
||||
def sql(now: Timestamp) =
|
||||
updateRow(
|
||||
table,
|
||||
and(oid.is(v.oid), cid.is(v.cid)),
|
||||
commas(
|
||||
cid.setTo(v.cid),
|
||||
name.setTo(v.name),
|
||||
street.setTo(v.street),
|
||||
zip.setTo(v.zip),
|
||||
city.setTo(v.city),
|
||||
country.setTo(v.country),
|
||||
notes.setTo(v.notes),
|
||||
updated.setTo(now)
|
||||
DML.update(
|
||||
T,
|
||||
T.oid === v.oid && T.cid === v.cid,
|
||||
DML.set(
|
||||
T.cid.setTo(v.cid),
|
||||
T.name.setTo(v.name),
|
||||
T.street.setTo(v.street),
|
||||
T.zip.setTo(v.zip),
|
||||
T.city.setTo(v.city),
|
||||
T.country.setTo(v.country),
|
||||
T.notes.setTo(v.notes),
|
||||
T.updated.setTo(now)
|
||||
)
|
||||
)
|
||||
for {
|
||||
now <- Timestamp.current[ConnectionIO]
|
||||
n <- sql(now).update.run
|
||||
n <- sql(now)
|
||||
} yield n
|
||||
}
|
||||
|
||||
def existsByName(coll: Ident, oname: String): ConnectionIO[Boolean] =
|
||||
selectCount(oid, table, and(cid.is(coll), name.is(oname)))
|
||||
run(select(count(T.oid)), from(T), T.cid === coll && T.name === oname)
|
||||
.query[Int]
|
||||
.unique
|
||||
.map(_ > 0)
|
||||
|
||||
def findById(id: Ident): ConnectionIO[Option[ROrganization]] = {
|
||||
val sql = selectSimple(all, table, cid.is(id))
|
||||
val sql = run(select(T.all), from(T), T.cid === id)
|
||||
sql.query[ROrganization].option
|
||||
}
|
||||
|
||||
def find(coll: Ident, orgName: String): ConnectionIO[Option[ROrganization]] = {
|
||||
val sql = selectSimple(all, table, and(cid.is(coll), name.is(orgName)))
|
||||
val sql = run(select(T.all), from(T), T.cid === coll && T.name === orgName)
|
||||
sql.query[ROrganization].option
|
||||
}
|
||||
|
||||
def findLike(coll: Ident, orgName: String): ConnectionIO[Vector[IdRef]] =
|
||||
selectSimple(List(oid, name), table, and(cid.is(coll), name.lowerLike(orgName)))
|
||||
run(select(T.oid, T.name), from(T), T.cid === coll && T.name.like(orgName))
|
||||
.query[IdRef]
|
||||
.to[Vector]
|
||||
|
||||
@ -102,42 +115,38 @@ object ROrganization {
|
||||
contactKind: ContactKind,
|
||||
value: String
|
||||
): ConnectionIO[Vector[IdRef]] = {
|
||||
val CC = RContact.Columns
|
||||
val q = fr"SELECT DISTINCT" ++ commas(oid.prefix("o").f, name.prefix("o").f) ++
|
||||
fr"FROM" ++ table ++ fr"o" ++
|
||||
fr"INNER JOIN" ++ RContact.table ++ fr"c ON" ++ CC.orgId
|
||||
.prefix("c")
|
||||
.is(oid.prefix("o")) ++
|
||||
fr"WHERE" ++ and(
|
||||
cid.prefix("o").is(coll),
|
||||
CC.kind.prefix("c").is(contactKind),
|
||||
CC.value.prefix("c").lowerLike(value)
|
||||
val c = RContact.as("c")
|
||||
val o = ROrganization.as("o")
|
||||
runDistinct(
|
||||
select(o.oid, o.name),
|
||||
from(o).innerJoin(c, c.orgId === o.oid),
|
||||
where(
|
||||
o.cid === coll,
|
||||
c.kind === contactKind,
|
||||
c.value.like(value)
|
||||
)
|
||||
|
||||
q.query[IdRef].to[Vector]
|
||||
).query[IdRef].to[Vector]
|
||||
}
|
||||
|
||||
def findAll(
|
||||
coll: Ident,
|
||||
order: Columns.type => Column
|
||||
order: Table => Column[_]
|
||||
): Stream[ConnectionIO, ROrganization] = {
|
||||
val sql = selectSimple(all, table, cid.is(coll)) ++ orderBy(order(Columns).f)
|
||||
sql.query[ROrganization].stream
|
||||
val sql = Select(select(T.all), from(T), T.cid === coll).orderBy(order(T))
|
||||
sql.build.query[ROrganization].stream
|
||||
}
|
||||
|
||||
def findAllRef(
|
||||
coll: Ident,
|
||||
nameQ: Option[String],
|
||||
order: Columns.type => Column
|
||||
order: Table => Column[_]
|
||||
): ConnectionIO[Vector[IdRef]] = {
|
||||
val q = Seq(cid.is(coll)) ++ (nameQ match {
|
||||
case Some(str) => Seq(name.lowerLike(s"%${str.toLowerCase}%"))
|
||||
case None => Seq.empty
|
||||
})
|
||||
val sql = selectSimple(List(oid, name), table, and(q)) ++ orderBy(order(Columns).f)
|
||||
sql.query[IdRef].to[Vector]
|
||||
val nameFilter = nameQ.map(s => T.name.like(s"%${s.toLowerCase}%"))
|
||||
val sql = Select(select(T.oid, T.name), from(T), T.cid === coll &&? nameFilter)
|
||||
.orderBy(order(T))
|
||||
sql.build.query[IdRef].to[Vector]
|
||||
}
|
||||
|
||||
def delete(id: Ident, coll: Ident): ConnectionIO[Int] =
|
||||
deleteFrom(table, and(oid.is(id), cid.is(coll))).update.run
|
||||
DML.delete(T, T.oid === id && T.cid === coll)
|
||||
}
|
||||
|
@ -1,11 +1,12 @@
|
||||
package docspell.store.records
|
||||
|
||||
import cats.data.NonEmptyList
|
||||
import cats.effect._
|
||||
import cats.implicits._
|
||||
|
||||
import docspell.common._
|
||||
import docspell.store.impl.Column
|
||||
import docspell.store.impl.Implicits._
|
||||
import docspell.store.qb.DSL._
|
||||
import docspell.store.qb._
|
||||
|
||||
import com.github.eikek.calev.CalEvent
|
||||
import doobie._
|
||||
@ -107,23 +108,23 @@ object RPeriodicTask {
|
||||
)(implicit E: Encoder[A]): F[RPeriodicTask] =
|
||||
create[F](enabled, task, group, E(args).noSpaces, subject, submitter, priority, timer)
|
||||
|
||||
val table = fr"periodic_task"
|
||||
final case class Table(alias: Option[String]) extends TableDef {
|
||||
val tableName = "periodic_task"
|
||||
|
||||
object Columns {
|
||||
val id = Column("id")
|
||||
val enabled = Column("enabled")
|
||||
val task = Column("task")
|
||||
val group = Column("group_")
|
||||
val args = Column("args")
|
||||
val subject = Column("subject")
|
||||
val submitter = Column("submitter")
|
||||
val priority = Column("priority")
|
||||
val worker = Column("worker")
|
||||
val marked = Column("marked")
|
||||
val timer = Column("timer")
|
||||
val nextrun = Column("nextrun")
|
||||
val created = Column("created")
|
||||
val all = List(
|
||||
val id = Column[Ident]("id", this)
|
||||
val enabled = Column[Boolean]("enabled", this)
|
||||
val task = Column[Ident]("task", this)
|
||||
val group = Column[Ident]("group_", this)
|
||||
val args = Column[String]("args", this)
|
||||
val subject = Column[String]("subject", this)
|
||||
val submitter = Column[Ident]("submitter", this)
|
||||
val priority = Column[Priority]("priority", this)
|
||||
val worker = Column[Ident]("worker", this)
|
||||
val marked = Column[Timestamp]("marked", this)
|
||||
val timer = Column[CalEvent]("timer", this)
|
||||
val nextrun = Column[Timestamp]("nextrun", this)
|
||||
val created = Column[Timestamp]("created", this)
|
||||
val all = NonEmptyList.of[Column[_]](
|
||||
id,
|
||||
enabled,
|
||||
task,
|
||||
@ -140,39 +141,37 @@ object RPeriodicTask {
|
||||
)
|
||||
}
|
||||
|
||||
import Columns._
|
||||
val T = Table(None)
|
||||
def as(alias: String): Table =
|
||||
Table(Some(alias))
|
||||
|
||||
def insert(v: RPeriodicTask): ConnectionIO[Int] = {
|
||||
val sql = insertRow(
|
||||
table,
|
||||
all,
|
||||
def insert(v: RPeriodicTask): ConnectionIO[Int] =
|
||||
DML.insert(
|
||||
T,
|
||||
T.all,
|
||||
fr"${v.id},${v.enabled},${v.task},${v.group},${v.args}," ++
|
||||
fr"${v.subject},${v.submitter},${v.priority},${v.worker}," ++
|
||||
fr"${v.marked},${v.timer},${v.nextrun},${v.created}"
|
||||
)
|
||||
sql.update.run
|
||||
}
|
||||
|
||||
def update(v: RPeriodicTask): ConnectionIO[Int] = {
|
||||
val sql = updateRow(
|
||||
table,
|
||||
id.is(v.id),
|
||||
commas(
|
||||
enabled.setTo(v.enabled),
|
||||
group.setTo(v.group),
|
||||
args.setTo(v.args),
|
||||
subject.setTo(v.subject),
|
||||
submitter.setTo(v.submitter),
|
||||
priority.setTo(v.priority),
|
||||
worker.setTo(v.worker),
|
||||
marked.setTo(v.marked),
|
||||
timer.setTo(v.timer),
|
||||
nextrun.setTo(v.nextrun)
|
||||
def update(v: RPeriodicTask): ConnectionIO[Int] =
|
||||
DML.update(
|
||||
T,
|
||||
T.id === v.id,
|
||||
DML.set(
|
||||
T.enabled.setTo(v.enabled),
|
||||
T.group.setTo(v.group),
|
||||
T.args.setTo(v.args),
|
||||
T.subject.setTo(v.subject),
|
||||
T.submitter.setTo(v.submitter),
|
||||
T.priority.setTo(v.priority),
|
||||
T.worker.setTo(v.worker),
|
||||
T.marked.setTo(v.marked),
|
||||
T.timer.setTo(v.timer),
|
||||
T.nextrun.setTo(v.nextrun)
|
||||
)
|
||||
)
|
||||
sql.update.run
|
||||
}
|
||||
|
||||
def exists(pid: Ident): ConnectionIO[Boolean] =
|
||||
selectCount(id, table, id.is(pid)).query[Int].unique.map(_ > 0)
|
||||
run(select(count(T.id)), from(T), T.id === pid).query[Int].unique.map(_ > 0)
|
||||
}
|
||||
|
@ -6,8 +6,8 @@ import cats.effect._
|
||||
import fs2.Stream
|
||||
|
||||
import docspell.common.{IdRef, _}
|
||||
import docspell.store.impl.Implicits._
|
||||
import docspell.store.impl._
|
||||
import docspell.store.qb.DSL._
|
||||
import docspell.store.qb._
|
||||
|
||||
import doobie._
|
||||
import doobie.implicits._
|
||||
@ -31,22 +31,22 @@ object RPerson {
|
||||
implicit val personEq: Eq[RPerson] =
|
||||
Eq.by(_.pid)
|
||||
|
||||
val table = fr"person"
|
||||
final case class Table(alias: Option[String]) extends TableDef {
|
||||
val tableName = "person"
|
||||
|
||||
object Columns {
|
||||
val pid = Column("pid")
|
||||
val cid = Column("cid")
|
||||
val name = Column("name")
|
||||
val street = Column("street")
|
||||
val zip = Column("zip")
|
||||
val city = Column("city")
|
||||
val country = Column("country")
|
||||
val notes = Column("notes")
|
||||
val concerning = Column("concerning")
|
||||
val created = Column("created")
|
||||
val updated = Column("updated")
|
||||
val oid = Column("oid")
|
||||
val all = List(
|
||||
val pid = Column[Ident]("pid", this)
|
||||
val cid = Column[Ident]("cid", this)
|
||||
val name = Column[String]("name", this)
|
||||
val street = Column[String]("street", this)
|
||||
val zip = Column[String]("zip", this)
|
||||
val city = Column[String]("city", this)
|
||||
val country = Column[String]("country", this)
|
||||
val notes = Column[String]("notes", this)
|
||||
val concerning = Column[Boolean]("concerning", this)
|
||||
val created = Column[Timestamp]("created", this)
|
||||
val updated = Column[Timestamp]("updated", this)
|
||||
val oid = Column[Ident]("oid", this)
|
||||
val all = NonEmptyList.of[Column[_]](
|
||||
pid,
|
||||
cid,
|
||||
name,
|
||||
@ -62,54 +62,54 @@ object RPerson {
|
||||
)
|
||||
}
|
||||
|
||||
import Columns._
|
||||
val T = Table(None)
|
||||
def as(alias: String): Table =
|
||||
Table(Some(alias))
|
||||
|
||||
def insert(v: RPerson): ConnectionIO[Int] = {
|
||||
val sql = insertRow(
|
||||
table,
|
||||
all,
|
||||
def insert(v: RPerson): ConnectionIO[Int] =
|
||||
DML.insert(
|
||||
T,
|
||||
T.all,
|
||||
fr"${v.pid},${v.cid},${v.name},${v.street},${v.zip},${v.city},${v.country},${v.notes},${v.concerning},${v.created},${v.updated},${v.oid}"
|
||||
)
|
||||
sql.update.run
|
||||
}
|
||||
|
||||
def update(v: RPerson): ConnectionIO[Int] = {
|
||||
def sql(now: Timestamp) =
|
||||
updateRow(
|
||||
table,
|
||||
and(pid.is(v.pid), cid.is(v.cid)),
|
||||
commas(
|
||||
cid.setTo(v.cid),
|
||||
name.setTo(v.name),
|
||||
street.setTo(v.street),
|
||||
zip.setTo(v.zip),
|
||||
city.setTo(v.city),
|
||||
country.setTo(v.country),
|
||||
concerning.setTo(v.concerning),
|
||||
notes.setTo(v.notes),
|
||||
oid.setTo(v.oid),
|
||||
updated.setTo(now)
|
||||
DML.update(
|
||||
T,
|
||||
T.pid === v.pid && T.cid === v.cid,
|
||||
DML.set(
|
||||
T.cid.setTo(v.cid),
|
||||
T.name.setTo(v.name),
|
||||
T.street.setTo(v.street),
|
||||
T.zip.setTo(v.zip),
|
||||
T.city.setTo(v.city),
|
||||
T.country.setTo(v.country),
|
||||
T.concerning.setTo(v.concerning),
|
||||
T.notes.setTo(v.notes),
|
||||
T.oid.setTo(v.oid),
|
||||
T.updated.setTo(now)
|
||||
)
|
||||
)
|
||||
for {
|
||||
now <- Timestamp.current[ConnectionIO]
|
||||
n <- sql(now).update.run
|
||||
n <- sql(now)
|
||||
} yield n
|
||||
}
|
||||
|
||||
def existsByName(coll: Ident, pname: String): ConnectionIO[Boolean] =
|
||||
selectCount(pid, table, and(cid.is(coll), name.is(pname)))
|
||||
run(select(count(T.pid)), from(T), T.cid === coll && T.name === pname)
|
||||
.query[Int]
|
||||
.unique
|
||||
.map(_ > 0)
|
||||
|
||||
def findById(id: Ident): ConnectionIO[Option[RPerson]] = {
|
||||
val sql = selectSimple(all, table, cid.is(id))
|
||||
val sql = run(select(T.all), from(T), T.cid === id)
|
||||
sql.query[RPerson].option
|
||||
}
|
||||
|
||||
def find(coll: Ident, personName: String): ConnectionIO[Option[RPerson]] = {
|
||||
val sql = selectSimple(all, table, and(cid.is(coll), name.is(personName)))
|
||||
val sql = run(select(T.all), from(T), T.cid === coll && T.name === personName)
|
||||
sql.query[RPerson].option
|
||||
}
|
||||
|
||||
@ -118,10 +118,10 @@ object RPerson {
|
||||
personName: String,
|
||||
concerningOnly: Boolean
|
||||
): ConnectionIO[Vector[IdRef]] =
|
||||
selectSimple(
|
||||
List(pid, name),
|
||||
table,
|
||||
and(cid.is(coll), concerning.is(concerningOnly), name.lowerLike(personName))
|
||||
run(
|
||||
select(T.pid, T.name),
|
||||
from(T),
|
||||
where(T.cid === coll, T.concerning === concerningOnly, T.name.like(personName))
|
||||
).query[IdRef].to[Vector]
|
||||
|
||||
def findLike(
|
||||
@ -130,53 +130,52 @@ object RPerson {
|
||||
value: String,
|
||||
concerningOnly: Boolean
|
||||
): ConnectionIO[Vector[IdRef]] = {
|
||||
val CC = RContact.Columns
|
||||
val q = fr"SELECT DISTINCT" ++ commas(pid.prefix("p").f, name.prefix("p").f) ++
|
||||
fr"FROM" ++ table ++ fr"p" ++
|
||||
fr"INNER JOIN" ++ RContact.table ++ fr"c ON" ++ CC.personId
|
||||
.prefix("c")
|
||||
.is(pid.prefix("p")) ++
|
||||
fr"WHERE" ++ and(
|
||||
cid.prefix("p").is(coll),
|
||||
CC.kind.prefix("c").is(contactKind),
|
||||
concerning.prefix("p").is(concerningOnly),
|
||||
CC.value.prefix("c").lowerLike(value)
|
||||
)
|
||||
val p = RPerson.as("p")
|
||||
val c = RContact.as("c")
|
||||
|
||||
q.query[IdRef].to[Vector]
|
||||
runDistinct(
|
||||
select(p.pid, p.name),
|
||||
from(p).innerJoin(c, p.pid === c.personId),
|
||||
where(
|
||||
p.cid === coll,
|
||||
c.kind === contactKind,
|
||||
p.concerning === concerningOnly,
|
||||
c.value.like(value)
|
||||
)
|
||||
).query[IdRef].to[Vector]
|
||||
}
|
||||
|
||||
def findAll(
|
||||
coll: Ident,
|
||||
order: Columns.type => Column
|
||||
order: Table => Column[_]
|
||||
): Stream[ConnectionIO, RPerson] = {
|
||||
val sql = selectSimple(all, table, cid.is(coll)) ++ orderBy(order(Columns).f)
|
||||
sql.query[RPerson].stream
|
||||
val sql = Select(select(T.all), from(T), T.cid === coll).orderBy(order(T))
|
||||
sql.build.query[RPerson].stream
|
||||
}
|
||||
|
||||
def findAllRef(
|
||||
coll: Ident,
|
||||
nameQ: Option[String],
|
||||
order: Columns.type => Column
|
||||
order: Table => Column[_]
|
||||
): ConnectionIO[Vector[IdRef]] = {
|
||||
val q = Seq(cid.is(coll)) ++ (nameQ match {
|
||||
case Some(str) => Seq(name.lowerLike(s"%${str.toLowerCase}%"))
|
||||
case None => Seq.empty
|
||||
})
|
||||
val sql = selectSimple(List(pid, name), table, and(q)) ++ orderBy(order(Columns).f)
|
||||
sql.query[IdRef].to[Vector]
|
||||
|
||||
val nameFilter = nameQ.map(s => T.name.like(s"%${s.toLowerCase}%"))
|
||||
|
||||
val sql = Select(select(T.pid, T.name), from(T), T.cid === coll &&? nameFilter)
|
||||
.orderBy(order(T))
|
||||
sql.build.query[IdRef].to[Vector]
|
||||
}
|
||||
|
||||
def delete(personId: Ident, coll: Ident): ConnectionIO[Int] =
|
||||
deleteFrom(table, and(pid.is(personId), cid.is(coll))).update.run
|
||||
DML.delete(T, T.pid === personId && T.cid === coll)
|
||||
|
||||
def findOrganization(ids: Set[Ident]): ConnectionIO[Vector[PersonRef]] = {
|
||||
val cols = Seq(pid, name, oid)
|
||||
def findOrganization(ids: Set[Ident]): ConnectionIO[Vector[PersonRef]] =
|
||||
NonEmptyList.fromList(ids.toList) match {
|
||||
case Some(nel) =>
|
||||
selectSimple(cols, table, pid.isIn(nel)).query[PersonRef].to[Vector]
|
||||
run(select(T.pid, T.name, T.oid), from(T), T.pid.in(nel))
|
||||
.query[PersonRef]
|
||||
.to[Vector]
|
||||
case None =>
|
||||
Sync[ConnectionIO].pure(Vector.empty)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,11 +1,12 @@
|
||||
package docspell.store.records
|
||||
|
||||
import cats.data.NonEmptyList
|
||||
import cats.effect.Sync
|
||||
import cats.implicits._
|
||||
|
||||
import docspell.common._
|
||||
import docspell.store.impl.Implicits._
|
||||
import docspell.store.impl._
|
||||
import docspell.store.qb.DSL._
|
||||
import docspell.store.qb._
|
||||
|
||||
import doobie._
|
||||
import doobie.implicits._
|
||||
@ -13,18 +14,20 @@ import doobie.implicits._
|
||||
case class RRememberMe(id: Ident, accountId: AccountId, created: Timestamp, uses: Int) {}
|
||||
|
||||
object RRememberMe {
|
||||
final case class Table(alias: Option[String]) extends TableDef {
|
||||
val tableName = "rememberme"
|
||||
|
||||
val table = fr"rememberme"
|
||||
|
||||
object Columns {
|
||||
val id = Column("id")
|
||||
val cid = Column("cid")
|
||||
val username = Column("login")
|
||||
val created = Column("created")
|
||||
val uses = Column("uses")
|
||||
val all = List(id, cid, username, created, uses)
|
||||
val id = Column[Ident]("id", this)
|
||||
val cid = Column[Ident]("cid", this)
|
||||
val username = Column[Ident]("login", this)
|
||||
val created = Column[Timestamp]("created", this)
|
||||
val uses = Column[Int]("uses", this)
|
||||
val all = NonEmptyList.of[Column[_]](id, cid, username, created, uses)
|
||||
}
|
||||
import Columns._
|
||||
|
||||
private val T = Table(None)
|
||||
def as(alias: String): Table =
|
||||
Table(Some(alias))
|
||||
|
||||
def generate[F[_]: Sync](account: AccountId): F[RRememberMe] =
|
||||
for {
|
||||
@ -33,29 +36,29 @@ object RRememberMe {
|
||||
} yield RRememberMe(i, account, c, 0)
|
||||
|
||||
def insert(v: RRememberMe): ConnectionIO[Int] =
|
||||
insertRow(
|
||||
table,
|
||||
all,
|
||||
DML.insert(
|
||||
T,
|
||||
T.all,
|
||||
fr"${v.id},${v.accountId.collective},${v.accountId.user},${v.created},${v.uses}"
|
||||
).update.run
|
||||
)
|
||||
|
||||
def insertNew(acc: AccountId): ConnectionIO[RRememberMe] =
|
||||
generate[ConnectionIO](acc).flatMap(v => insert(v).map(_ => v))
|
||||
|
||||
def findById(rid: Ident): ConnectionIO[Option[RRememberMe]] =
|
||||
selectSimple(all, table, id.is(rid)).query[RRememberMe].option
|
||||
run(select(T.all), from(T), T.id === rid).query[RRememberMe].option
|
||||
|
||||
def delete(rid: Ident): ConnectionIO[Int] =
|
||||
deleteFrom(table, id.is(rid)).update.run
|
||||
DML.delete(T, T.id === rid)
|
||||
|
||||
def incrementUse(rid: Ident): ConnectionIO[Int] =
|
||||
updateRow(table, id.is(rid), uses.increment(1)).update.run
|
||||
DML.update(T, T.id === rid, DML.set(T.uses.increment(1)))
|
||||
|
||||
def useRememberMe(
|
||||
rid: Ident,
|
||||
minCreated: Timestamp
|
||||
): ConnectionIO[Option[RRememberMe]] = {
|
||||
val get = selectSimple(all, table, and(id.is(rid), created.isGt(minCreated)))
|
||||
val get = run(select(T.all), from(T), T.id === rid && T.created > minCreated)
|
||||
.query[RRememberMe]
|
||||
.option
|
||||
for {
|
||||
@ -65,5 +68,5 @@ object RRememberMe {
|
||||
}
|
||||
|
||||
def deleteOlderThan(ts: Timestamp): ConnectionIO[Int] =
|
||||
deleteFrom(table, created.isLt(ts)).update.run
|
||||
DML.delete(T, T.created < ts)
|
||||
}
|
||||
|
@ -7,8 +7,8 @@ import cats.implicits._
|
||||
import fs2.Stream
|
||||
|
||||
import docspell.common._
|
||||
import docspell.store.impl.Column
|
||||
import docspell.store.impl.Implicits._
|
||||
import docspell.store.qb.DSL._
|
||||
import docspell.store.qb._
|
||||
|
||||
import doobie._
|
||||
import doobie.implicits._
|
||||
@ -78,20 +78,21 @@ object RSentMail {
|
||||
si <- OptionT.liftF(RSentMailItem[ConnectionIO](itemId, sm.id, Some(sm.created)))
|
||||
} yield (sm, si)
|
||||
|
||||
val table = fr"sentmail"
|
||||
final case class Table(alias: Option[String]) extends TableDef {
|
||||
|
||||
object Columns {
|
||||
val id = Column("id")
|
||||
val uid = Column("uid")
|
||||
val messageId = Column("message_id")
|
||||
val sender = Column("sender")
|
||||
val connName = Column("conn_name")
|
||||
val subject = Column("subject")
|
||||
val recipients = Column("recipients")
|
||||
val body = Column("body")
|
||||
val created = Column("created")
|
||||
val tableName = "sentmail"
|
||||
|
||||
val all = List(
|
||||
val id = Column[Ident]("id", this)
|
||||
val uid = Column[Ident]("uid", this)
|
||||
val messageId = Column[String]("message_id", this)
|
||||
val sender = Column[MailAddress]("sender", this)
|
||||
val connName = Column[Ident]("conn_name", this)
|
||||
val subject = Column[String]("subject", this)
|
||||
val recipients = Column[List[MailAddress]]("recipients", this)
|
||||
val body = Column[String]("body", this)
|
||||
val created = Column[Timestamp]("created", this)
|
||||
|
||||
val all = NonEmptyList.of[Column[_]](
|
||||
id,
|
||||
uid,
|
||||
messageId,
|
||||
@ -104,27 +105,29 @@ object RSentMail {
|
||||
)
|
||||
}
|
||||
|
||||
import Columns._
|
||||
private val T = Table(None)
|
||||
def as(alias: String): Table =
|
||||
Table(Some(alias))
|
||||
|
||||
def insert(v: RSentMail): ConnectionIO[Int] =
|
||||
insertRow(
|
||||
table,
|
||||
all,
|
||||
DML.insert(
|
||||
T,
|
||||
T.all,
|
||||
sql"${v.id},${v.uid},${v.messageId},${v.sender},${v.connName},${v.subject},${v.recipients},${v.body},${v.created}"
|
||||
).update.run
|
||||
)
|
||||
|
||||
def findByUser(userId: Ident): Stream[ConnectionIO, RSentMail] =
|
||||
selectSimple(all, table, uid.is(userId)).query[RSentMail].stream
|
||||
run(select(T.all), from(T), T.uid === userId).query[RSentMail].stream
|
||||
|
||||
def delete(mailId: Ident): ConnectionIO[Int] =
|
||||
deleteFrom(table, id.is(mailId)).update.run
|
||||
DML.delete(T, T.id === mailId)
|
||||
|
||||
def deleteByItem(item: Ident): ConnectionIO[Int] =
|
||||
for {
|
||||
list <- RSentMailItem.findSentMailIdsByItem(item)
|
||||
n1 <- RSentMailItem.deleteAllByItem(item)
|
||||
n0 <- NonEmptyList.fromList(list.toList) match {
|
||||
case Some(nel) => deleteFrom(table, id.isIn(nel)).update.run
|
||||
case Some(nel) => DML.delete(T, T.id.in(nel))
|
||||
case None => 0.pure[ConnectionIO]
|
||||
}
|
||||
} yield n0 + n1
|
||||
|
@ -1,11 +1,12 @@
|
||||
package docspell.store.records
|
||||
|
||||
import cats.data.NonEmptyList
|
||||
import cats.effect._
|
||||
import cats.implicits._
|
||||
|
||||
import docspell.common._
|
||||
import docspell.store.impl.Column
|
||||
import docspell.store.impl.Implicits._
|
||||
import docspell.store.qb.DSL._
|
||||
import docspell.store.qb._
|
||||
|
||||
import doobie._
|
||||
import doobie.implicits._
|
||||
@ -29,15 +30,15 @@ object RSentMailItem {
|
||||
now <- created.map(_.pure[F]).getOrElse(Timestamp.current[F])
|
||||
} yield RSentMailItem(id, itemId, sentmailId, now)
|
||||
|
||||
val table = fr"sentmailitem"
|
||||
final case class Table(alias: Option[String]) extends TableDef {
|
||||
val tableName = "sentmailitem"
|
||||
|
||||
object Columns {
|
||||
val id = Column("id")
|
||||
val itemId = Column("item_id")
|
||||
val sentMailId = Column("sentmail_id")
|
||||
val created = Column("created")
|
||||
val id = Column[Ident]("id", this)
|
||||
val itemId = Column[Ident]("item_id", this)
|
||||
val sentMailId = Column[Ident]("sentmail_id", this)
|
||||
val created = Column[Timestamp]("created", this)
|
||||
|
||||
val all = List(
|
||||
val all = NonEmptyList.of[Column[_]](
|
||||
id,
|
||||
itemId,
|
||||
sentMailId,
|
||||
@ -45,21 +46,23 @@ object RSentMailItem {
|
||||
)
|
||||
}
|
||||
|
||||
import Columns._
|
||||
private val T = Table(None)
|
||||
def as(alias: String): Table =
|
||||
Table(Some(alias))
|
||||
|
||||
def insert(v: RSentMailItem): ConnectionIO[Int] =
|
||||
insertRow(
|
||||
table,
|
||||
all,
|
||||
DML.insert(
|
||||
T,
|
||||
T.all,
|
||||
sql"${v.id},${v.itemId},${v.sentMailId},${v.created}"
|
||||
).update.run
|
||||
)
|
||||
|
||||
def deleteMail(mailId: Ident): ConnectionIO[Int] =
|
||||
deleteFrom(table, sentMailId.is(mailId)).update.run
|
||||
DML.delete(T, T.sentMailId === mailId)
|
||||
|
||||
def findSentMailIdsByItem(item: Ident): ConnectionIO[Set[Ident]] =
|
||||
selectSimple(Seq(sentMailId), table, itemId.is(item)).query[Ident].to[Set]
|
||||
run(select(T.sentMailId.s), from(T), T.itemId === item).query[Ident].to[Set]
|
||||
|
||||
def deleteAllByItem(item: Ident): ConnectionIO[Int] =
|
||||
deleteFrom(table, itemId.is(item)).update.run
|
||||
DML.delete(T, T.itemId === item)
|
||||
}
|
||||
|
@ -1,8 +1,10 @@
|
||||
package docspell.store.records
|
||||
|
||||
import cats.data.NonEmptyList
|
||||
|
||||
import docspell.common._
|
||||
import docspell.store.impl.Implicits._
|
||||
import docspell.store.impl._
|
||||
import docspell.store.qb.DSL._
|
||||
import docspell.store.qb._
|
||||
|
||||
import doobie._
|
||||
import doobie.implicits._
|
||||
@ -26,23 +28,22 @@ case class RSource(
|
||||
|
||||
object RSource {
|
||||
|
||||
val table = fr"source"
|
||||
final case class Table(alias: Option[String]) extends TableDef {
|
||||
val tableName = "source"
|
||||
|
||||
object Columns {
|
||||
|
||||
val sid = Column("sid")
|
||||
val cid = Column("cid")
|
||||
val abbrev = Column("abbrev")
|
||||
val description = Column("description")
|
||||
val counter = Column("counter")
|
||||
val enabled = Column("enabled")
|
||||
val priority = Column("priority")
|
||||
val created = Column("created")
|
||||
val folder = Column("folder_id")
|
||||
val fileFilter = Column("file_filter")
|
||||
val sid = Column[Ident]("sid", this)
|
||||
val cid = Column[Ident]("cid", this)
|
||||
val abbrev = Column[String]("abbrev", this)
|
||||
val description = Column[String]("description", this)
|
||||
val counter = Column[Int]("counter", this)
|
||||
val enabled = Column[Boolean]("enabled", this)
|
||||
val priority = Column[Priority]("priority", this)
|
||||
val created = Column[Timestamp]("created", this)
|
||||
val folder = Column[Ident]("folder_id", this)
|
||||
val fileFilter = Column[Glob]("file_filter", this)
|
||||
|
||||
val all =
|
||||
List(
|
||||
NonEmptyList.of[Column[_]](
|
||||
sid,
|
||||
cid,
|
||||
abbrev,
|
||||
@ -56,48 +57,51 @@ object RSource {
|
||||
)
|
||||
}
|
||||
|
||||
import Columns._
|
||||
def as(alias: String): Table =
|
||||
Table(Some(alias))
|
||||
|
||||
def insert(v: RSource): ConnectionIO[Int] = {
|
||||
val sql = insertRow(
|
||||
val table = Table(None)
|
||||
|
||||
def insert(v: RSource): ConnectionIO[Int] =
|
||||
DML.insert(
|
||||
table,
|
||||
all,
|
||||
table.all,
|
||||
fr"${v.sid},${v.cid},${v.abbrev},${v.description},${v.counter},${v.enabled},${v.priority},${v.created},${v.folderId},${v.fileFilter}"
|
||||
)
|
||||
sql.update.run
|
||||
}
|
||||
|
||||
def updateNoCounter(v: RSource): ConnectionIO[Int] = {
|
||||
val sql = updateRow(
|
||||
def updateNoCounter(v: RSource): ConnectionIO[Int] =
|
||||
DML.update(
|
||||
table,
|
||||
and(sid.is(v.sid), cid.is(v.cid)),
|
||||
commas(
|
||||
cid.setTo(v.cid),
|
||||
abbrev.setTo(v.abbrev),
|
||||
description.setTo(v.description),
|
||||
enabled.setTo(v.enabled),
|
||||
priority.setTo(v.priority),
|
||||
folder.setTo(v.folderId),
|
||||
fileFilter.setTo(v.fileFilter)
|
||||
where(table.sid === v.sid, table.cid === v.cid),
|
||||
DML.set(
|
||||
table.cid.setTo(v.cid),
|
||||
table.abbrev.setTo(v.abbrev),
|
||||
table.description.setTo(v.description),
|
||||
table.enabled.setTo(v.enabled),
|
||||
table.priority.setTo(v.priority),
|
||||
table.folder.setTo(v.folderId),
|
||||
table.fileFilter.setTo(v.fileFilter)
|
||||
)
|
||||
)
|
||||
sql.update.run
|
||||
}
|
||||
|
||||
def incrementCounter(source: String, coll: Ident): ConnectionIO[Int] =
|
||||
updateRow(
|
||||
DML.update(
|
||||
table,
|
||||
and(abbrev.is(source), cid.is(coll)),
|
||||
counter.f ++ fr"=" ++ counter.f ++ fr"+ 1"
|
||||
).update.run
|
||||
where(table.abbrev === source, table.cid === coll),
|
||||
DML.set(table.counter.increment(1))
|
||||
)
|
||||
|
||||
def existsById(id: Ident): ConnectionIO[Boolean] = {
|
||||
val sql = selectCount(sid, table, sid.is(id))
|
||||
val sql = run(select(count(table.sid)), from(table), where(table.sid === id))
|
||||
sql.query[Int].unique.map(_ > 0)
|
||||
}
|
||||
|
||||
def existsByAbbrev(coll: Ident, abb: String): ConnectionIO[Boolean] = {
|
||||
val sql = selectCount(sid, table, and(cid.is(coll), abbrev.is(abb)))
|
||||
val sql = run(
|
||||
select(count(table.sid)),
|
||||
from(table),
|
||||
where(table.cid === coll, table.abbrev === abb)
|
||||
)
|
||||
sql.query[Int].unique.map(_ > 0)
|
||||
}
|
||||
|
||||
@ -105,25 +109,34 @@ object RSource {
|
||||
findEnabledSql(id).query[RSource].option
|
||||
|
||||
private[records] def findEnabledSql(id: Ident): Fragment =
|
||||
selectSimple(all, table, and(sid.is(id), enabled.is(true)))
|
||||
run(select(table.all), from(table), where(table.sid === id, table.enabled === true))
|
||||
|
||||
def findCollective(sourceId: Ident): ConnectionIO[Option[Ident]] =
|
||||
selectSimple(List(cid), table, sid.is(sourceId)).query[Ident].option
|
||||
run(select(table.cid), from(table), table.sid === sourceId).query[Ident].option
|
||||
|
||||
def findAll(
|
||||
coll: Ident,
|
||||
order: Columns.type => Column
|
||||
order: Table => Column[_]
|
||||
): ConnectionIO[Vector[RSource]] =
|
||||
findAllSql(coll, order).query[RSource].to[Vector]
|
||||
|
||||
private[records] def findAllSql(coll: Ident, order: Columns.type => Column): Fragment =
|
||||
selectSimple(all, table, cid.is(coll)) ++ orderBy(order(Columns).f)
|
||||
private[records] def findAllSql(
|
||||
coll: Ident,
|
||||
order: Table => Column[_]
|
||||
): Fragment = {
|
||||
val t = RSource.as("s")
|
||||
Select(select(t.all), from(t), t.cid === coll).orderBy(order(t)).build
|
||||
}
|
||||
|
||||
def delete(sourceId: Ident, coll: Ident): ConnectionIO[Int] =
|
||||
deleteFrom(table, and(sid.is(sourceId), cid.is(coll))).update.run
|
||||
DML.delete(table, where(table.sid === sourceId, table.cid === coll))
|
||||
|
||||
def removeFolder(folderId: Ident): ConnectionIO[Int] = {
|
||||
val empty: Option[Ident] = None
|
||||
updateRow(table, folder.is(folderId), folder.setTo(empty)).update.run
|
||||
DML.update(
|
||||
table,
|
||||
where(table.folder === folderId),
|
||||
DML.set(table.folder.setTo(empty))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -4,8 +4,8 @@ import cats.data.NonEmptyList
|
||||
import cats.implicits._
|
||||
|
||||
import docspell.common._
|
||||
import docspell.store.impl.Implicits._
|
||||
import docspell.store.impl._
|
||||
import docspell.store.qb.DSL._
|
||||
import docspell.store.qb._
|
||||
|
||||
import doobie._
|
||||
import doobie.implicits._
|
||||
@ -19,101 +19,97 @@ case class RTag(
|
||||
) {}
|
||||
|
||||
object RTag {
|
||||
final case class Table(alias: Option[String]) extends TableDef {
|
||||
val tableName = "tag"
|
||||
|
||||
val table = fr"tag"
|
||||
|
||||
object Columns {
|
||||
val tid = Column("tid")
|
||||
val cid = Column("cid")
|
||||
val name = Column("name")
|
||||
val category = Column("category")
|
||||
val created = Column("created")
|
||||
val all = List(tid, cid, name, category, created)
|
||||
val tid = Column[Ident]("tid", this)
|
||||
val cid = Column[Ident]("cid", this)
|
||||
val name = Column[String]("name", this)
|
||||
val category = Column[String]("category", this)
|
||||
val created = Column[Timestamp]("created", this)
|
||||
val all = NonEmptyList.of[Column[_]](tid, cid, name, category, created)
|
||||
}
|
||||
import Columns._
|
||||
val T = Table(None)
|
||||
def as(alias: String): Table =
|
||||
Table(Some(alias))
|
||||
|
||||
def insert(v: RTag): ConnectionIO[Int] = {
|
||||
val sql =
|
||||
insertRow(
|
||||
table,
|
||||
all,
|
||||
def insert(v: RTag): ConnectionIO[Int] =
|
||||
DML.insert(
|
||||
T,
|
||||
T.all,
|
||||
fr"${v.tagId},${v.collective},${v.name},${v.category},${v.created}"
|
||||
)
|
||||
sql.update.run
|
||||
}
|
||||
|
||||
def update(v: RTag): ConnectionIO[Int] = {
|
||||
val sql = updateRow(
|
||||
table,
|
||||
and(tid.is(v.tagId), cid.is(v.collective)),
|
||||
commas(
|
||||
cid.setTo(v.collective),
|
||||
name.setTo(v.name),
|
||||
category.setTo(v.category)
|
||||
def update(v: RTag): ConnectionIO[Int] =
|
||||
DML.update(
|
||||
T,
|
||||
T.tid === v.tagId && T.cid === v.collective,
|
||||
DML.set(
|
||||
T.cid.setTo(v.collective),
|
||||
T.name.setTo(v.name),
|
||||
T.category.setTo(v.category)
|
||||
)
|
||||
)
|
||||
sql.update.run
|
||||
}
|
||||
|
||||
def findById(id: Ident): ConnectionIO[Option[RTag]] = {
|
||||
val sql = selectSimple(all, table, tid.is(id))
|
||||
val sql = run(select(T.all), from(T), T.tid === id)
|
||||
sql.query[RTag].option
|
||||
}
|
||||
|
||||
def findByIdAndCollective(id: Ident, coll: Ident): ConnectionIO[Option[RTag]] = {
|
||||
val sql = selectSimple(all, table, and(tid.is(id), cid.is(coll)))
|
||||
val sql = run(select(T.all), from(T), T.tid === id && T.cid === coll)
|
||||
sql.query[RTag].option
|
||||
}
|
||||
|
||||
def existsByName(tag: RTag): ConnectionIO[Boolean] = {
|
||||
val sql = selectCount(
|
||||
tid,
|
||||
table,
|
||||
and(cid.is(tag.collective), name.is(tag.name))
|
||||
)
|
||||
val sql =
|
||||
run(select(count(T.tid)), from(T), T.cid === tag.collective && T.name === tag.name)
|
||||
sql.query[Int].unique.map(_ > 0)
|
||||
}
|
||||
|
||||
def findAll(
|
||||
coll: Ident,
|
||||
nameQ: Option[String],
|
||||
order: Columns.type => Column
|
||||
order: Table => Column[_]
|
||||
): ConnectionIO[Vector[RTag]] = {
|
||||
val q = Seq(cid.is(coll)) ++ (nameQ match {
|
||||
case Some(str) => Seq(name.lowerLike(s"%${str.toLowerCase}%"))
|
||||
case None => Seq.empty
|
||||
})
|
||||
val sql = selectSimple(all, table, and(q)) ++ orderBy(order(Columns).f)
|
||||
sql.query[RTag].to[Vector]
|
||||
val nameFilter = nameQ.map(s => T.name.like(s"%${s.toLowerCase}%"))
|
||||
val sql =
|
||||
Select(select(T.all), from(T), T.cid === coll &&? nameFilter).orderBy(order(T))
|
||||
sql.build.query[RTag].to[Vector]
|
||||
}
|
||||
|
||||
def findAllById(ids: List[Ident]): ConnectionIO[Vector[RTag]] =
|
||||
selectSimple(all, table, tid.isIn(ids.map(id => sql"$id").toSeq))
|
||||
NonEmptyList.fromList(ids) match {
|
||||
case Some(nel) =>
|
||||
run(select(T.all), from(T), T.tid.in(nel))
|
||||
.query[RTag]
|
||||
.to[Vector]
|
||||
case None =>
|
||||
Vector.empty.pure[ConnectionIO]
|
||||
}
|
||||
|
||||
def findByItem(itemId: Ident): ConnectionIO[Vector[RTag]] = {
|
||||
val rcol = all.map(_.prefix("t"))
|
||||
(selectSimple(
|
||||
rcol,
|
||||
table ++ fr"t," ++ RTagItem.table ++ fr"i",
|
||||
and(
|
||||
RTagItem.Columns.itemId.prefix("i").is(itemId),
|
||||
RTagItem.Columns.tagId.prefix("i").is(tid.prefix("t"))
|
||||
)
|
||||
) ++ orderBy(name.prefix("t").asc)).query[RTag].to[Vector]
|
||||
val ti = RTagItem.as("i")
|
||||
val t = RTag.as("t")
|
||||
val sql =
|
||||
Select(
|
||||
select(t.all),
|
||||
from(t).innerJoin(ti, ti.tagId === t.tid),
|
||||
ti.itemId === itemId
|
||||
).orderBy(t.name.asc)
|
||||
sql.build.query[RTag].to[Vector]
|
||||
}
|
||||
|
||||
def findBySource(source: Ident): ConnectionIO[Vector[RTag]] = {
|
||||
val rcol = all.map(_.prefix("t"))
|
||||
(selectSimple(
|
||||
rcol,
|
||||
table ++ fr"t," ++ RTagSource.table ++ fr"s",
|
||||
and(
|
||||
RTagSource.Columns.sourceId.prefix("s").is(source),
|
||||
RTagSource.Columns.tagId.prefix("s").is(tid.prefix("t"))
|
||||
)
|
||||
) ++ orderBy(name.prefix("t").asc)).query[RTag].to[Vector]
|
||||
val s = RTagSource.as("s")
|
||||
val t = RTag.as("t")
|
||||
val sql =
|
||||
Select(
|
||||
select(t.all),
|
||||
from(t).innerJoin(s, s.tagId === t.tid),
|
||||
s.sourceId === source
|
||||
).orderBy(t.name.asc)
|
||||
sql.build.query[RTag].to[Vector]
|
||||
}
|
||||
|
||||
def findAllByNameOrId(
|
||||
@ -121,16 +117,22 @@ object RTag {
|
||||
coll: Ident
|
||||
): ConnectionIO[Vector[RTag]] = {
|
||||
val idList =
|
||||
NonEmptyList.fromList(nameOrIds.flatMap(s => Ident.fromString(s).toOption)).toSeq
|
||||
val nameList = NonEmptyList.fromList(nameOrIds.map(_.toLowerCase)).toSeq
|
||||
|
||||
val cond = idList.flatMap(ids => Seq(tid.isIn(ids))) ++
|
||||
nameList.flatMap(ns => Seq(name.isLowerIn(ns)))
|
||||
|
||||
if (cond.isEmpty) Vector.empty.pure[ConnectionIO]
|
||||
else selectSimple(all, table, and(cid.is(coll), or(cond))).query[RTag].to[Vector]
|
||||
NonEmptyList.fromList(nameOrIds.flatMap(s => Ident.fromString(s).toOption))
|
||||
val nameList = NonEmptyList.fromList(nameOrIds.map(_.toLowerCase))
|
||||
(idList, nameList) match {
|
||||
case (Some(ids), _) =>
|
||||
val cond =
|
||||
T.cid === coll && (T.tid.in(ids) ||? nameList.map(names => T.name.in(names)))
|
||||
run(select(T.all), from(T), cond).query[RTag].to[Vector]
|
||||
case (_, Some(names)) =>
|
||||
val cond =
|
||||
T.cid === coll && (T.name.in(names) ||? idList.map(ids => T.tid.in(ids)))
|
||||
run(select(T.all), from(T), cond).query[RTag].to[Vector]
|
||||
case (None, None) =>
|
||||
Vector.empty.pure[ConnectionIO]
|
||||
}
|
||||
}
|
||||
|
||||
def delete(tagId: Ident, coll: Ident): ConnectionIO[Int] =
|
||||
deleteFrom(table, and(tid.is(tagId), cid.is(coll))).update.run
|
||||
DML.delete(T, T.tid === tagId && T.cid === coll)
|
||||
}
|
||||
|
@ -4,8 +4,8 @@ import cats.data.NonEmptyList
|
||||
import cats.implicits._
|
||||
|
||||
import docspell.common._
|
||||
import docspell.store.impl.Implicits._
|
||||
import docspell.store.impl._
|
||||
import docspell.store.qb.DSL._
|
||||
import docspell.store.qb._
|
||||
|
||||
import doobie._
|
||||
import doobie.implicits._
|
||||
@ -13,41 +13,37 @@ import doobie.implicits._
|
||||
case class RTagItem(tagItemId: Ident, itemId: Ident, tagId: Ident) {}
|
||||
|
||||
object RTagItem {
|
||||
final case class Table(alias: Option[String]) extends TableDef {
|
||||
val tableName = "tagitem"
|
||||
|
||||
val table = fr"tagitem"
|
||||
|
||||
object Columns {
|
||||
val tagItemId = Column("tagitemid")
|
||||
val itemId = Column("itemid")
|
||||
val tagId = Column("tid")
|
||||
val all = List(tagItemId, itemId, tagId)
|
||||
val tagItemId = Column[Ident]("tagitemid", this)
|
||||
val itemId = Column[Ident]("itemid", this)
|
||||
val tagId = Column[Ident]("tid", this)
|
||||
val all = NonEmptyList.of[Column[_]](tagItemId, itemId, tagId)
|
||||
}
|
||||
import Columns._
|
||||
val T = Table(None)
|
||||
def as(alias: String): Table =
|
||||
Table(Some(alias))
|
||||
|
||||
def insert(v: RTagItem): ConnectionIO[Int] =
|
||||
insertRow(table, all, fr"${v.tagItemId},${v.itemId},${v.tagId}").update.run
|
||||
DML.insert(T, T.all, fr"${v.tagItemId},${v.itemId},${v.tagId}")
|
||||
|
||||
def deleteItemTags(item: Ident): ConnectionIO[Int] =
|
||||
deleteFrom(table, itemId.is(item)).update.run
|
||||
DML.delete(T, T.itemId === item)
|
||||
|
||||
def deleteItemTags(items: NonEmptyList[Ident], cid: Ident): ConnectionIO[Int] = {
|
||||
val itemsFiltered =
|
||||
RItem.filterItemsFragment(items, cid)
|
||||
val sql = fr"DELETE FROM" ++ table ++ fr"WHERE" ++ itemId.isIn(itemsFiltered)
|
||||
|
||||
sql.update.run
|
||||
}
|
||||
def deleteItemTags(items: NonEmptyList[Ident], cid: Ident): ConnectionIO[Int] =
|
||||
DML.delete(T, T.itemId.in(RItem.filterItemsFragment(items, cid)))
|
||||
|
||||
def deleteTag(tid: Ident): ConnectionIO[Int] =
|
||||
deleteFrom(table, tagId.is(tid)).update.run
|
||||
DML.delete(T, T.tagId === tid)
|
||||
|
||||
def findByItem(item: Ident): ConnectionIO[Vector[RTagItem]] =
|
||||
selectSimple(all, table, itemId.is(item)).query[RTagItem].to[Vector]
|
||||
run(select(T.all), from(T), T.itemId === item).query[RTagItem].to[Vector]
|
||||
|
||||
def findAllIn(item: Ident, tags: Seq[Ident]): ConnectionIO[Vector[RTagItem]] =
|
||||
NonEmptyList.fromList(tags.toList) match {
|
||||
case Some(nel) =>
|
||||
selectSimple(all, table, and(itemId.is(item), tagId.isIn(nel)))
|
||||
run(select(T.all), from(T), T.itemId === item && T.tagId.in(nel))
|
||||
.query[RTagItem]
|
||||
.to[Vector]
|
||||
case None =>
|
||||
@ -59,7 +55,7 @@ object RTagItem {
|
||||
case None =>
|
||||
0.pure[ConnectionIO]
|
||||
case Some(nel) =>
|
||||
deleteFrom(table, and(itemId.is(item), tagId.isIn(nel))).update.run
|
||||
DML.delete(T, T.itemId === item && T.tagId.in(nel))
|
||||
}
|
||||
|
||||
def setAllTags(item: Ident, tags: Seq[Ident]): ConnectionIO[Int] =
|
||||
@ -69,11 +65,12 @@ object RTagItem {
|
||||
entities <- tags.toList.traverse(tagId =>
|
||||
Ident.randomId[ConnectionIO].map(id => RTagItem(id, item, tagId))
|
||||
)
|
||||
n <- insertRows(
|
||||
table,
|
||||
all,
|
||||
n <- DML
|
||||
.insertMany(
|
||||
T,
|
||||
T.all,
|
||||
entities.map(v => fr"${v.tagItemId},${v.itemId},${v.tagId}")
|
||||
).update.run
|
||||
)
|
||||
} yield n
|
||||
|
||||
def appendTags(item: Ident, tags: List[Ident]): ConnectionIO[Int] =
|
||||
|
@ -1,11 +1,12 @@
|
||||
package docspell.store.records
|
||||
|
||||
import cats.data.NonEmptyList
|
||||
import cats.effect.Sync
|
||||
import cats.implicits._
|
||||
|
||||
import docspell.common._
|
||||
import docspell.store.impl.Implicits._
|
||||
import docspell.store.impl._
|
||||
import docspell.store.qb.DSL._
|
||||
import docspell.store.qb._
|
||||
|
||||
import doobie._
|
||||
import doobie.implicits._
|
||||
@ -13,31 +14,33 @@ import doobie.implicits._
|
||||
case class RTagSource(id: Ident, sourceId: Ident, tagId: Ident) {}
|
||||
|
||||
object RTagSource {
|
||||
final case class Table(alias: Option[String]) extends TableDef {
|
||||
val tableName = "tagsource"
|
||||
|
||||
val table = fr"tagsource"
|
||||
|
||||
object Columns {
|
||||
val id = Column("id")
|
||||
val sourceId = Column("source_id")
|
||||
val tagId = Column("tag_id")
|
||||
val all = List(id, sourceId, tagId)
|
||||
val id = Column[Ident]("id", this)
|
||||
val sourceId = Column[Ident]("source_id", this)
|
||||
val tagId = Column[Ident]("tag_id", this)
|
||||
val all = NonEmptyList.of[Column[_]](id, sourceId, tagId)
|
||||
}
|
||||
import Columns._
|
||||
|
||||
private val t = Table(None)
|
||||
def as(alias: String): Table =
|
||||
Table(Some(alias))
|
||||
|
||||
def createNew[F[_]: Sync](source: Ident, tag: Ident): F[RTagSource] =
|
||||
Ident.randomId[F].map(id => RTagSource(id, source, tag))
|
||||
|
||||
def insert(v: RTagSource): ConnectionIO[Int] =
|
||||
insertRow(table, all, fr"${v.id},${v.sourceId},${v.tagId}").update.run
|
||||
DML.insert(t, t.all, fr"${v.id},${v.sourceId},${v.tagId}")
|
||||
|
||||
def deleteSourceTags(source: Ident): ConnectionIO[Int] =
|
||||
deleteFrom(table, sourceId.is(source)).update.run
|
||||
DML.delete(t, t.sourceId === source)
|
||||
|
||||
def deleteTag(tid: Ident): ConnectionIO[Int] =
|
||||
deleteFrom(table, tagId.is(tid)).update.run
|
||||
DML.delete(t, t.tagId === tid)
|
||||
|
||||
def findBySource(source: Ident): ConnectionIO[Vector[RTagSource]] =
|
||||
selectSimple(all, table, sourceId.is(source)).query[RTagSource].to[Vector]
|
||||
run(select(t.all), from(t), t.sourceId === source).query[RTagSource].to[Vector]
|
||||
|
||||
def setAllTags(source: Ident, tags: Seq[Ident]): ConnectionIO[Int] =
|
||||
if (tags.isEmpty) 0.pure[ConnectionIO]
|
||||
@ -46,11 +49,12 @@ object RTagSource {
|
||||
entities <- tags.toList.traverse(tagId =>
|
||||
Ident.randomId[ConnectionIO].map(id => RTagSource(id, source, tagId))
|
||||
)
|
||||
n <- insertRows(
|
||||
table,
|
||||
all,
|
||||
n <- DML
|
||||
.insertMany(
|
||||
t,
|
||||
t.all,
|
||||
entities.map(v => fr"${v.id},${v.sourceId},${v.tagId}")
|
||||
).update.run
|
||||
)
|
||||
} yield n
|
||||
|
||||
}
|
||||
|
@ -1,8 +1,10 @@
|
||||
package docspell.store.records
|
||||
|
||||
import cats.data.NonEmptyList
|
||||
|
||||
import docspell.common._
|
||||
import docspell.store.impl.Implicits._
|
||||
import docspell.store.impl._
|
||||
import docspell.store.qb.DSL._
|
||||
import docspell.store.qb._
|
||||
|
||||
import doobie._
|
||||
import doobie.implicits._
|
||||
@ -20,86 +22,108 @@ case class RUser(
|
||||
) {}
|
||||
|
||||
object RUser {
|
||||
final case class Table(alias: Option[String]) extends TableDef {
|
||||
val tableName = "user_"
|
||||
|
||||
val table = fr"user_"
|
||||
|
||||
object Columns {
|
||||
val uid = Column("uid")
|
||||
val cid = Column("cid")
|
||||
val login = Column("login")
|
||||
val password = Column("password")
|
||||
val state = Column("state")
|
||||
val email = Column("email")
|
||||
val loginCount = Column("logincount")
|
||||
val lastLogin = Column("lastlogin")
|
||||
val created = Column("created")
|
||||
val uid = Column[Ident]("uid", this)
|
||||
val login = Column[Ident]("login", this)
|
||||
val cid = Column[Ident]("cid", this)
|
||||
val password = Column[Password]("password", this)
|
||||
val state = Column[UserState]("state", this)
|
||||
val email = Column[String]("email", this)
|
||||
val loginCount = Column[Int]("logincount", this)
|
||||
val lastLogin = Column[Timestamp]("lastlogin", this)
|
||||
val created = Column[Timestamp]("created", this)
|
||||
|
||||
val all =
|
||||
List(uid, login, cid, password, state, email, loginCount, lastLogin, created)
|
||||
NonEmptyList.of[Column[_]](
|
||||
uid,
|
||||
login,
|
||||
cid,
|
||||
password,
|
||||
state,
|
||||
email,
|
||||
loginCount,
|
||||
lastLogin,
|
||||
created
|
||||
)
|
||||
}
|
||||
|
||||
import Columns._
|
||||
def as(alias: String): Table =
|
||||
Table(Some(alias))
|
||||
|
||||
def insert(v: RUser): ConnectionIO[Int] = {
|
||||
val sql = insertRow(
|
||||
table,
|
||||
Columns.all,
|
||||
val t = Table(None)
|
||||
DML.insert(
|
||||
t,
|
||||
t.all,
|
||||
fr"${v.uid},${v.login},${v.cid},${v.password},${v.state},${v.email},${v.loginCount},${v.lastLogin},${v.created}"
|
||||
)
|
||||
sql.update.run
|
||||
}
|
||||
|
||||
def update(v: RUser): ConnectionIO[Int] = {
|
||||
val sql = updateRow(
|
||||
table,
|
||||
and(login.is(v.login), cid.is(v.cid)),
|
||||
commas(
|
||||
state.setTo(v.state),
|
||||
email.setTo(v.email),
|
||||
loginCount.setTo(v.loginCount),
|
||||
lastLogin.setTo(v.lastLogin)
|
||||
val t = Table(None)
|
||||
DML.update(
|
||||
t,
|
||||
t.login === v.login && t.cid === v.cid,
|
||||
DML.set(
|
||||
t.state.setTo(v.state),
|
||||
t.email.setTo(v.email),
|
||||
t.loginCount.setTo(v.loginCount),
|
||||
t.lastLogin.setTo(v.lastLogin)
|
||||
)
|
||||
)
|
||||
sql.update.run
|
||||
}
|
||||
|
||||
def exists(loginName: Ident): ConnectionIO[Boolean] =
|
||||
selectCount(uid, table, login.is(loginName)).query[Int].unique.map(_ > 0)
|
||||
def exists(loginName: Ident): ConnectionIO[Boolean] = {
|
||||
val t = Table(None)
|
||||
run(select(count(t.uid)), from(t), t.login === loginName).query[Int].unique.map(_ > 0)
|
||||
}
|
||||
|
||||
def findByAccount(aid: AccountId): ConnectionIO[Option[RUser]] = {
|
||||
val sql = selectSimple(all, table, and(cid.is(aid.collective), login.is(aid.user)))
|
||||
val t = Table(None)
|
||||
val sql =
|
||||
run(select(t.all), from(t), t.cid === aid.collective && t.login === aid.user)
|
||||
sql.query[RUser].option
|
||||
}
|
||||
|
||||
def findById(userId: Ident): ConnectionIO[Option[RUser]] = {
|
||||
val sql = selectSimple(all, table, uid.is(userId))
|
||||
val t = Table(None)
|
||||
val sql = run(select(t.all), from(t), t.uid === userId)
|
||||
sql.query[RUser].option
|
||||
}
|
||||
|
||||
def findAll(coll: Ident, order: Columns.type => Column): ConnectionIO[Vector[RUser]] = {
|
||||
val sql = selectSimple(all, table, cid.is(coll)) ++ orderBy(order(Columns).f)
|
||||
def findAll(coll: Ident, order: Table => Column[_]): ConnectionIO[Vector[RUser]] = {
|
||||
val t = Table(None)
|
||||
val sql = Select(select(t.all), from(t), t.cid === coll).orderBy(order(t)).build
|
||||
sql.query[RUser].to[Vector]
|
||||
}
|
||||
|
||||
def updateLogin(accountId: AccountId): ConnectionIO[Int] =
|
||||
currentTime.flatMap(t =>
|
||||
updateRow(
|
||||
table,
|
||||
and(cid.is(accountId.collective), login.is(accountId.user)),
|
||||
commas(
|
||||
loginCount.f ++ fr"=" ++ loginCount.f ++ fr"+ 1",
|
||||
lastLogin.setTo(t)
|
||||
def updateLogin(accountId: AccountId): ConnectionIO[Int] = {
|
||||
val t = Table(None)
|
||||
def stmt(now: Timestamp) =
|
||||
DML.update(
|
||||
t,
|
||||
t.cid === accountId.collective && t.login === accountId.user,
|
||||
DML.set(
|
||||
t.loginCount.increment(1),
|
||||
t.lastLogin.setTo(now)
|
||||
)
|
||||
).update.run
|
||||
)
|
||||
|
||||
def updatePassword(accountId: AccountId, hashedPass: Password): ConnectionIO[Int] =
|
||||
updateRow(
|
||||
table,
|
||||
and(cid.is(accountId.collective), login.is(accountId.user)),
|
||||
password.setTo(hashedPass)
|
||||
).update.run
|
||||
|
||||
def delete(user: Ident, coll: Ident): ConnectionIO[Int] =
|
||||
deleteFrom(table, and(cid.is(coll), login.is(user))).update.run
|
||||
Timestamp.current[ConnectionIO].flatMap(stmt)
|
||||
}
|
||||
|
||||
def updatePassword(accountId: AccountId, hashedPass: Password): ConnectionIO[Int] = {
|
||||
val t = Table(None)
|
||||
DML.update(
|
||||
t,
|
||||
t.cid === accountId.collective && t.login === accountId.user,
|
||||
DML.set(t.password.setTo(hashedPass))
|
||||
)
|
||||
}
|
||||
|
||||
def delete(user: Ident, coll: Ident): ConnectionIO[Int] = {
|
||||
val t = Table(None)
|
||||
DML.delete(t, t.cid === coll && t.login === user)
|
||||
}
|
||||
}
|
||||
|
@ -1,12 +1,12 @@
|
||||
package docspell.store.records
|
||||
|
||||
import cats.data.OptionT
|
||||
import cats.data.{NonEmptyList, OptionT}
|
||||
import cats.effect._
|
||||
import cats.implicits._
|
||||
|
||||
import docspell.common._
|
||||
import docspell.store.impl.Column
|
||||
import docspell.store.impl.Implicits._
|
||||
import docspell.store.qb.DSL._
|
||||
import docspell.store.qb._
|
||||
|
||||
import doobie._
|
||||
import doobie.implicits._
|
||||
@ -101,24 +101,24 @@ object RUserEmail {
|
||||
mailReplyTo,
|
||||
now
|
||||
)
|
||||
final case class Table(alias: Option[String]) extends TableDef {
|
||||
|
||||
val table = fr"useremail"
|
||||
val tableName = "useremail"
|
||||
|
||||
object Columns {
|
||||
val id = Column("id")
|
||||
val uid = Column("uid")
|
||||
val name = Column("name")
|
||||
val smtpHost = Column("smtp_host")
|
||||
val smtpPort = Column("smtp_port")
|
||||
val smtpUser = Column("smtp_user")
|
||||
val smtpPass = Column("smtp_password")
|
||||
val smtpSsl = Column("smtp_ssl")
|
||||
val smtpCertCheck = Column("smtp_certcheck")
|
||||
val mailFrom = Column("mail_from")
|
||||
val mailReplyTo = Column("mail_replyto")
|
||||
val created = Column("created")
|
||||
val id = Column[Ident]("id", this)
|
||||
val uid = Column[Ident]("uid", this)
|
||||
val name = Column[Ident]("name", this)
|
||||
val smtpHost = Column[String]("smtp_host", this)
|
||||
val smtpPort = Column[Int]("smtp_port", this)
|
||||
val smtpUser = Column[String]("smtp_user", this)
|
||||
val smtpPass = Column[Password]("smtp_password", this)
|
||||
val smtpSsl = Column[SSLType]("smtp_ssl", this)
|
||||
val smtpCertCheck = Column[Boolean]("smtp_certcheck", this)
|
||||
val mailFrom = Column[MailAddress]("mail_from", this)
|
||||
val mailReplyTo = Column[MailAddress]("mail_replyto", this)
|
||||
val created = Column[Timestamp]("created", this)
|
||||
|
||||
val all = List(
|
||||
val all = NonEmptyList.of[Column[_]](
|
||||
id,
|
||||
uid,
|
||||
name,
|
||||
@ -134,54 +134,61 @@ object RUserEmail {
|
||||
)
|
||||
}
|
||||
|
||||
import Columns._
|
||||
def as(alias: String): Table =
|
||||
Table(Some(alias))
|
||||
|
||||
def insert(v: RUserEmail): ConnectionIO[Int] =
|
||||
insertRow(
|
||||
table,
|
||||
all,
|
||||
def insert(v: RUserEmail): ConnectionIO[Int] = {
|
||||
val t = Table(None)
|
||||
DML.insert(
|
||||
t,
|
||||
t.all,
|
||||
sql"${v.id},${v.uid},${v.name},${v.smtpHost},${v.smtpPort},${v.smtpUser},${v.smtpPassword},${v.smtpSsl},${v.smtpCertCheck},${v.mailFrom},${v.mailReplyTo},${v.created}"
|
||||
).update.run
|
||||
|
||||
def update(eId: Ident, v: RUserEmail): ConnectionIO[Int] =
|
||||
updateRow(
|
||||
table,
|
||||
id.is(eId),
|
||||
commas(
|
||||
name.setTo(v.name),
|
||||
smtpHost.setTo(v.smtpHost),
|
||||
smtpPort.setTo(v.smtpPort),
|
||||
smtpUser.setTo(v.smtpUser),
|
||||
smtpPass.setTo(v.smtpPassword),
|
||||
smtpSsl.setTo(v.smtpSsl),
|
||||
smtpCertCheck.setTo(v.smtpCertCheck),
|
||||
mailFrom.setTo(v.mailFrom),
|
||||
mailReplyTo.setTo(v.mailReplyTo)
|
||||
)
|
||||
).update.run
|
||||
}
|
||||
|
||||
def findByUser(userId: Ident): ConnectionIO[Vector[RUserEmail]] =
|
||||
selectSimple(all, table, uid.is(userId)).query[RUserEmail].to[Vector]
|
||||
def update(eId: Ident, v: RUserEmail): ConnectionIO[Int] = {
|
||||
val t = Table(None)
|
||||
DML.update(
|
||||
t,
|
||||
t.id === eId,
|
||||
DML.set(
|
||||
t.name.setTo(v.name),
|
||||
t.smtpHost.setTo(v.smtpHost),
|
||||
t.smtpPort.setTo(v.smtpPort),
|
||||
t.smtpUser.setTo(v.smtpUser),
|
||||
t.smtpPass.setTo(v.smtpPassword),
|
||||
t.smtpSsl.setTo(v.smtpSsl),
|
||||
t.smtpCertCheck.setTo(v.smtpCertCheck),
|
||||
t.mailFrom.setTo(v.mailFrom),
|
||||
t.mailReplyTo.setTo(v.mailReplyTo)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
def findByUser(userId: Ident): ConnectionIO[Vector[RUserEmail]] = {
|
||||
val t = Table(None)
|
||||
run(select(t.all), from(t), t.uid === userId).query[RUserEmail].to[Vector]
|
||||
}
|
||||
|
||||
private def findByAccount0(
|
||||
accId: AccountId,
|
||||
nameQ: Option[String],
|
||||
exact: Boolean
|
||||
): Query0[RUserEmail] = {
|
||||
val mUid = uid.prefix("m")
|
||||
val mName = name.prefix("m")
|
||||
val uId = RUser.Columns.uid.prefix("u")
|
||||
val uColl = RUser.Columns.cid.prefix("u")
|
||||
val uLogin = RUser.Columns.login.prefix("u")
|
||||
val from = table ++ fr"m INNER JOIN" ++ RUser.table ++ fr"u ON" ++ mUid.is(uId)
|
||||
val cond = Seq(uColl.is(accId.collective), uLogin.is(accId.user)) ++ (nameQ match {
|
||||
case Some(str) if exact => Seq(mName.is(str))
|
||||
case Some(str) => Seq(mName.lowerLike(s"%${str.toLowerCase}%"))
|
||||
case None => Seq.empty
|
||||
})
|
||||
val user = RUser.as("u")
|
||||
val email = as("m")
|
||||
|
||||
(selectSimple(all.map(_.prefix("m")), from, and(cond)) ++ orderBy(mName.f))
|
||||
.query[RUserEmail]
|
||||
val nameFilter = nameQ.map(s =>
|
||||
if (exact) email.name ==== s else email.name.likes(s"%${s.toLowerCase}%")
|
||||
)
|
||||
|
||||
val sql = Select(
|
||||
select(email.all),
|
||||
from(email).innerJoin(user, email.uid === user.uid),
|
||||
user.cid === accId.collective && user.login === accId.user &&? nameFilter
|
||||
).orderBy(email.name)
|
||||
|
||||
sql.build.query[RUserEmail]
|
||||
}
|
||||
|
||||
def findByAccount(
|
||||
@ -194,26 +201,26 @@ object RUserEmail {
|
||||
findByAccount0(accId, Some(name.id), true).option
|
||||
|
||||
def delete(accId: AccountId, connName: Ident): ConnectionIO[Int] = {
|
||||
val uId = RUser.Columns.uid
|
||||
val uColl = RUser.Columns.cid
|
||||
val uLogin = RUser.Columns.login
|
||||
val cond = Seq(uColl.is(accId.collective), uLogin.is(accId.user))
|
||||
val user = RUser.as("u")
|
||||
|
||||
deleteFrom(
|
||||
table,
|
||||
fr"uid in (" ++ selectSimple(Seq(uId), RUser.table, and(cond)) ++ fr") AND" ++ name
|
||||
.is(
|
||||
connName
|
||||
val subsel = Select(
|
||||
select(user.uid),
|
||||
from(user),
|
||||
user.cid === accId.collective && user.login === accId.user
|
||||
)
|
||||
).update.run
|
||||
|
||||
val t = Table(None)
|
||||
DML.delete(t, t.uid.in(subsel) && t.name === connName)
|
||||
}
|
||||
|
||||
def exists(accId: AccountId, name: Ident): ConnectionIO[Boolean] =
|
||||
getByName(accId, name).map(_.isDefined)
|
||||
|
||||
def exists(userId: Ident, connName: Ident): ConnectionIO[Boolean] =
|
||||
selectCount(id, table, and(uid.is(userId), name.is(connName)))
|
||||
def exists(userId: Ident, connName: Ident): ConnectionIO[Boolean] = {
|
||||
val t = Table(None)
|
||||
run(select(count(t.id)), from(t), t.uid === userId && t.name === connName)
|
||||
.query[Int]
|
||||
.unique
|
||||
.map(_ > 0)
|
||||
}
|
||||
}
|
||||
|
@ -1,12 +1,12 @@
|
||||
package docspell.store.records
|
||||
|
||||
import cats.data.OptionT
|
||||
import cats.data.{NonEmptyList, OptionT}
|
||||
import cats.effect._
|
||||
import cats.implicits._
|
||||
|
||||
import docspell.common._
|
||||
import docspell.store.impl.Column
|
||||
import docspell.store.impl.Implicits._
|
||||
import docspell.store.qb.DSL._
|
||||
import docspell.store.qb._
|
||||
|
||||
import doobie._
|
||||
import doobie.implicits._
|
||||
@ -92,21 +92,21 @@ object RUserImap {
|
||||
now
|
||||
)
|
||||
|
||||
val table = fr"userimap"
|
||||
final case class Table(alias: Option[String]) extends TableDef {
|
||||
val tableName = "userimap"
|
||||
|
||||
object Columns {
|
||||
val id = Column("id")
|
||||
val uid = Column("uid")
|
||||
val name = Column("name")
|
||||
val imapHost = Column("imap_host")
|
||||
val imapPort = Column("imap_port")
|
||||
val imapUser = Column("imap_user")
|
||||
val imapPass = Column("imap_password")
|
||||
val imapSsl = Column("imap_ssl")
|
||||
val imapCertCheck = Column("imap_certcheck")
|
||||
val created = Column("created")
|
||||
val id = Column[Ident]("id", this)
|
||||
val uid = Column[Ident]("uid", this)
|
||||
val name = Column[Ident]("name", this)
|
||||
val imapHost = Column[String]("imap_host", this)
|
||||
val imapPort = Column[Int]("imap_port", this)
|
||||
val imapUser = Column[String]("imap_user", this)
|
||||
val imapPass = Column[Password]("imap_password", this)
|
||||
val imapSsl = Column[SSLType]("imap_ssl", this)
|
||||
val imapCertCheck = Column[Boolean]("imap_certcheck", this)
|
||||
val created = Column[Timestamp]("created", this)
|
||||
|
||||
val all = List(
|
||||
val all = NonEmptyList.of[Column[_]](
|
||||
id,
|
||||
uid,
|
||||
name,
|
||||
@ -120,52 +120,62 @@ object RUserImap {
|
||||
)
|
||||
}
|
||||
|
||||
import Columns._
|
||||
def as(alias: String): Table =
|
||||
Table(Some(alias))
|
||||
|
||||
def insert(v: RUserImap): ConnectionIO[Int] =
|
||||
insertRow(
|
||||
table,
|
||||
all,
|
||||
def insert(v: RUserImap): ConnectionIO[Int] = {
|
||||
val t = Table(None)
|
||||
DML
|
||||
.insert(
|
||||
t,
|
||||
t.all,
|
||||
sql"${v.id},${v.uid},${v.name},${v.imapHost},${v.imapPort},${v.imapUser},${v.imapPassword},${v.imapSsl},${v.imapCertCheck},${v.created}"
|
||||
).update.run
|
||||
|
||||
def update(eId: Ident, v: RUserImap): ConnectionIO[Int] =
|
||||
updateRow(
|
||||
table,
|
||||
id.is(eId),
|
||||
commas(
|
||||
name.setTo(v.name),
|
||||
imapHost.setTo(v.imapHost),
|
||||
imapPort.setTo(v.imapPort),
|
||||
imapUser.setTo(v.imapUser),
|
||||
imapPass.setTo(v.imapPassword),
|
||||
imapSsl.setTo(v.imapSsl),
|
||||
imapCertCheck.setTo(v.imapCertCheck)
|
||||
)
|
||||
).update.run
|
||||
}
|
||||
|
||||
def findByUser(userId: Ident): ConnectionIO[Vector[RUserImap]] =
|
||||
selectSimple(all, table, uid.is(userId)).query[RUserImap].to[Vector]
|
||||
def update(eId: Ident, v: RUserImap): ConnectionIO[Int] = {
|
||||
val t = Table(None)
|
||||
DML.update(
|
||||
t,
|
||||
t.id === eId,
|
||||
DML.set(
|
||||
t.name.setTo(v.name),
|
||||
t.imapHost.setTo(v.imapHost),
|
||||
t.imapPort.setTo(v.imapPort),
|
||||
t.imapUser.setTo(v.imapUser),
|
||||
t.imapPass.setTo(v.imapPassword),
|
||||
t.imapSsl.setTo(v.imapSsl),
|
||||
t.imapCertCheck.setTo(v.imapCertCheck)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
def findByUser(userId: Ident): ConnectionIO[Vector[RUserImap]] = {
|
||||
val t = Table(None)
|
||||
run(select(t.all), from(t), t.uid === userId).query[RUserImap].to[Vector]
|
||||
}
|
||||
|
||||
private def findByAccount0(
|
||||
accId: AccountId,
|
||||
nameQ: Option[String],
|
||||
exact: Boolean
|
||||
): Query0[RUserImap] = {
|
||||
val mUid = uid.prefix("m")
|
||||
val mName = name.prefix("m")
|
||||
val uId = RUser.Columns.uid.prefix("u")
|
||||
val uColl = RUser.Columns.cid.prefix("u")
|
||||
val uLogin = RUser.Columns.login.prefix("u")
|
||||
val from = table ++ fr"m INNER JOIN" ++ RUser.table ++ fr"u ON" ++ mUid.is(uId)
|
||||
val cond = Seq(uColl.is(accId.collective), uLogin.is(accId.user)) ++ (nameQ match {
|
||||
case Some(str) if exact => Seq(mName.is(str))
|
||||
case Some(str) => Seq(mName.lowerLike(s"%${str.toLowerCase}%"))
|
||||
case None => Seq.empty
|
||||
})
|
||||
val m = RUserImap.as("m")
|
||||
val u = RUser.as("u")
|
||||
|
||||
(selectSimple(all.map(_.prefix("m")), from, and(cond)) ++ orderBy(mName.f))
|
||||
.query[RUserImap]
|
||||
val nameFilter =
|
||||
nameQ.map { str =>
|
||||
if (exact) m.name ==== str
|
||||
else m.name.likes(s"%${str.toLowerCase}%")
|
||||
}
|
||||
|
||||
val sql = Select(
|
||||
select(m.all),
|
||||
from(m).innerJoin(u, m.uid === u.uid),
|
||||
u.cid === accId.collective && u.login === accId.user &&? nameFilter
|
||||
).orderBy(m.name).build
|
||||
|
||||
sql.query[RUserImap]
|
||||
}
|
||||
|
||||
def findByAccount(
|
||||
@ -178,26 +188,25 @@ object RUserImap {
|
||||
findByAccount0(accId, Some(name.id), true).option
|
||||
|
||||
def delete(accId: AccountId, connName: Ident): ConnectionIO[Int] = {
|
||||
val uId = RUser.Columns.uid
|
||||
val uColl = RUser.Columns.cid
|
||||
val uLogin = RUser.Columns.login
|
||||
val cond = Seq(uColl.is(accId.collective), uLogin.is(accId.user))
|
||||
val t = Table(None)
|
||||
val u = RUser.as("u")
|
||||
val subsel =
|
||||
Select(select(u.uid), from(u), u.cid === accId.collective && u.login === accId.user)
|
||||
|
||||
deleteFrom(
|
||||
table,
|
||||
fr"uid in (" ++ selectSimple(Seq(uId), RUser.table, and(cond)) ++ fr") AND" ++ name
|
||||
.is(
|
||||
connName
|
||||
DML.delete(
|
||||
t,
|
||||
t.uid.in(subsel) && t.name === connName
|
||||
)
|
||||
).update.run
|
||||
}
|
||||
|
||||
def exists(accId: AccountId, name: Ident): ConnectionIO[Boolean] =
|
||||
getByName(accId, name).map(_.isDefined)
|
||||
|
||||
def exists(userId: Ident, connName: Ident): ConnectionIO[Boolean] =
|
||||
selectCount(id, table, and(uid.is(userId), name.is(connName)))
|
||||
def exists(userId: Ident, connName: Ident): ConnectionIO[Boolean] = {
|
||||
val t = Table(None)
|
||||
run(select(count(t.id)), from(t), t.uid === userId && t.name === connName)
|
||||
.query[Int]
|
||||
.unique
|
||||
.map(_ > 0)
|
||||
}
|
||||
}
|
||||
|
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