Generate a query string given an expression

Initialize share record and improve tests.
This commit is contained in:
eikek 2021-10-01 01:42:20 +02:00
parent d065abf7ac
commit de1baf725f
10 changed files with 718 additions and 16 deletions

View File

@ -123,9 +123,11 @@ object ItemQuery {
final case class ChecksumMatch(checksum: String) extends Expr final case class ChecksumMatch(checksum: String) extends Expr
final case class AttachId(id: String) extends Expr final case class AttachId(id: String) extends Expr
final case object ValidItemStates extends Expr /** A "private" expression is only visible in code, but cannot be parsed. */
final case object Trashed extends Expr sealed trait PrivateExpr extends Expr
final case object ValidItemsOrTrashed extends Expr final case object ValidItemStates extends PrivateExpr
final case object Trashed extends PrivateExpr
final case object ValidItemsOrTrashed extends PrivateExpr
// things that can be expressed with terms above // things that can be expressed with terms above
sealed trait MacroExpr extends Expr { sealed trait MacroExpr extends Expr {

View File

@ -8,12 +8,23 @@ package docspell.query
import cats.data.NonEmptyList import cats.data.NonEmptyList
import docspell.query.internal.ExprParser import docspell.query.internal.{ExprParser, ExprString, ExprUtil}
import docspell.query.internal.ExprUtil
object ItemQueryParser { object ItemQueryParser {
val PrivateExprError = ExprString.PrivateExprError
type PrivateExprError = ExprString.PrivateExprError
def parse(input: String): Either[ParseFailure, ItemQuery] = def parse(input: String): Either[ParseFailure, ItemQuery] =
parse0(input, expandMacros = true)
def parseKeepMacros(input: String): Either[ParseFailure, ItemQuery] =
parse0(input, expandMacros = false)
private def parse0(
input: String,
expandMacros: Boolean
): Either[ParseFailure, ItemQuery] =
if (input.isEmpty) if (input.isEmpty)
Left( Left(
ParseFailure("", 0, NonEmptyList.of(ParseFailure.SimpleMessage(0, "No input."))) ParseFailure("", 0, NonEmptyList.of(ParseFailure.SimpleMessage(0, "No input.")))
@ -24,9 +35,16 @@ object ItemQueryParser {
.parseQuery(in) .parseQuery(in)
.left .left
.map(ParseFailure.fromError(in)) .map(ParseFailure.fromError(in))
.map(q => q.copy(expr = ExprUtil.reduce(q.expr))) .map(q => q.copy(expr = ExprUtil.reduce(expandMacros)(q.expr)))
} }
def parseUnsafe(input: String): ItemQuery = def parseUnsafe(input: String): ItemQuery =
parse(input).fold(m => sys.error(m.render), identity) parse(input).fold(m => sys.error(m.render), identity)
def asString(q: ItemQuery.Expr): Either[PrivateExprError, String] =
ExprString(q)
def unsafeAsString(q: ItemQuery.Expr): String =
asString(q).fold(f => sys.error(s"Cannot expose private query part: $f"), identity)
} }

View File

@ -24,7 +24,7 @@ object BasicParser {
) )
private[this] val identChars: Set[Char] = private[this] val identChars: Set[Char] =
(('A' to 'Z') ++ ('a' to 'z') ++ ('0' to '9') ++ "-_.").toSet (('A' to 'Z') ++ ('a' to 'z') ++ ('0' to '9') ++ "-_.@").toSet
val parenAnd: P[Unit] = val parenAnd: P[Unit] =
P.stringIn(List("(&", "(and")).void <* ws0 P.stringIn(List("(&", "(and")).void <* ws0

View File

@ -0,0 +1,244 @@
/*
* Copyright 2020 Eike K. & Contributors
*
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
package docspell.query.internal
import java.time.Period
import docspell.query.Date
import docspell.query.Date.DateLiteral
import docspell.query.ItemQuery.Attr._
import docspell.query.ItemQuery.Expr._
import docspell.query.ItemQuery._
import docspell.query.internal.{Constants => C}
/** Creates the string representation for a given expression. The returned string can be
* parsed back to the expression using `ExprParser`. Note that expressions obtained from
* the `ItemQueryParser` have macros already expanded.
*
* It may fail when the expression contains non-public parts. Every expression that has
* been created by parsing a string, can be transformed back to a string. But an
* expression created via code may contain parts that cannot be transformed to a string.
*/
object ExprString {
final case class PrivateExprError(expr: Expr.PrivateExpr)
type Result = Either[PrivateExprError, String]
def apply(expr: Expr): Result =
expr match {
case AndExpr(inner) =>
val es = inner.traverse(ExprString.apply)
es.map(_.toList.mkString(" ")).map(els => s"(& $els )")
case OrExpr(inner) =>
val es = inner.traverse(ExprString.apply)
es.map(_.toList.mkString(" ")).map(els => s"(| $els )")
case NotExpr(inner) =>
inner match {
case NotExpr(inner2) =>
apply(inner2)
case _ =>
apply(inner).map(n => s"!$n")
}
case m: MacroExpr =>
Right(macroStr(m))
case DirectionExpr(v) =>
Right(s"${C.incoming}${C.like}${v}")
case InboxExpr(v) =>
Right(s"${C.inbox}${C.like}${v}")
case InExpr(attr, values) =>
val els = values.map(quote).toList.mkString(",")
Right(s"${attrStr(attr)}${C.in}$els")
case InDateExpr(attr, values) =>
val els = values.map(dateStr).toList.mkString(",")
Right(s"${attrStr(attr)}${C.in}$els")
case TagsMatch(op, values) =>
val els = values.map(quote).toList.mkString(",")
Right(s"${C.tag}${tagOpStr(op)}$els")
case TagIdsMatch(op, values) =>
val els = values.map(quote).toList.mkString(",")
Right(s"${C.tagId}${tagOpStr(op)}$els")
case Exists(field) =>
Right(s"${C.exist}${C.like}${attrStr(field)}")
case Fulltext(v) =>
Right(s"${C.content}${C.like}${quote(v)}")
case SimpleExpr(op, prop) =>
prop match {
case Property.StringProperty(attr, value) =>
Right(s"${stringAttr(attr)}${opStr(op)}${quote(value)}")
case Property.DateProperty(attr, value) =>
Right(s"${dateAttr(attr)}${opStr(op)}${dateStr(value)}")
case Property.IntProperty(attr, value) =>
Right(s"${attrStr(attr)}${opStr(op)}$value")
}
case TagCategoryMatch(op, values) =>
val els = values.map(quote).toList.mkString(",")
Right(s"${C.cat}${tagOpStr(op)}$els")
case CustomFieldMatch(name, op, value) =>
Right(s"${C.customField}:$name${opStr(op)}${quote(value)}")
case CustomFieldIdMatch(id, op, value) =>
Right(s"${C.customFieldId}:$id${opStr(op)}${quote(value)}")
case ChecksumMatch(cs) =>
Right(s"${C.checksum}${C.like}$cs")
case AttachId(aid) =>
Right(s"${C.attachId}${C.eqs}$aid")
case pe: PrivateExpr =>
// There is no parser equivalent for this
Left(PrivateExprError(pe))
}
private[internal] def macroStr(expr: Expr.MacroExpr): String =
expr match {
case Expr.NamesMacro(name) =>
s"${C.names}:${quote(name)}"
case Expr.YearMacro(_, year) =>
s"${C.year}:$year" //currently, only for Attr.Date
case Expr.ConcMacro(term) =>
s"${C.conc}:${quote(term)}"
case Expr.CorrMacro(term) =>
s"${C.corr}:${quote(term)}"
case Expr.DateRangeMacro(attr, left, right) =>
val name = attr match {
case Attr.CreatedDate =>
C.createdIn
case Attr.Date =>
C.dateIn
case Attr.DueDate =>
C.dueIn
}
(left, right) match {
case (_: Date.DateLiteral, Date.Calc(date, calc, period)) =>
s"$name:${dateStr(date)};${calcStr(calc)}${periodStr(period)}"
case (Date.Calc(date, calc, period), _: DateLiteral) =>
s"$name:${dateStr(date)};${calcStr(calc)}${periodStr(period)}"
case (Date.Calc(d1, _, p1), Date.Calc(_, _, _)) =>
s"$name:${dateStr(d1)};/${periodStr(p1)}"
case (_: DateLiteral, _: DateLiteral) =>
sys.error("Invalid date range")
}
}
private[internal] def dateStr(date: Date): String =
date match {
case Date.Today =>
"today"
case Date.Local(ld) =>
f"${ld.getYear}-${ld.getMonthValue}%02d-${ld.getDayOfMonth}%02d"
case Date.Millis(ms) =>
s"ms$ms"
case Date.Calc(date, calc, period) =>
val ds = dateStr(date)
s"$ds;${calcStr(calc)}${periodStr(period)}"
}
private[internal] def calcStr(c: Date.CalcDirection): String =
c match {
case Date.CalcDirection.Plus => "+"
case Date.CalcDirection.Minus => "-"
}
private[internal] def periodStr(p: Period): String =
if (p.toTotalMonths == 0) s"${p.getDays}d"
else s"${p.toTotalMonths}m"
private[internal] def attrStr(attr: Attr): String =
attr match {
case a: StringAttr => stringAttr(a)
case a: DateAttr => dateAttr(a)
case a: IntAttr => intAttr(a)
}
private[internal] def intAttr(attr: IntAttr): String =
attr match {
case AttachCount =>
Constants.attachCount
}
private[internal] def dateAttr(attr: DateAttr): String =
attr match {
case Attr.Date =>
Constants.date
case DueDate =>
Constants.due
case CreatedDate =>
Constants.created
}
private[internal] def stringAttr(attr: StringAttr): String =
attr match {
case Attr.ItemName =>
Constants.name
case Attr.ItemId =>
Constants.id
case Attr.ItemSource =>
Constants.source
case Attr.ItemNotes =>
Constants.notes
case Correspondent.OrgId =>
Constants.corrOrgId
case Correspondent.OrgName =>
Constants.corrOrgName
case Correspondent.PersonId =>
Constants.corrPersId
case Correspondent.PersonName =>
Constants.corrPersName
case Concerning.EquipId =>
Constants.concEquipId
case Concerning.EquipName =>
Constants.concEquipName
case Concerning.PersonId =>
Constants.concPersId
case Concerning.PersonName =>
Constants.concPersName
case Folder.FolderName =>
Constants.folder
case Folder.FolderId =>
Constants.folderId
}
private[internal] def opStr(op: Operator): String =
op match {
case Operator.Like => Constants.like.toString
case Operator.Gte => Constants.gte
case Operator.Lte => Constants.lte
case Operator.Eq => Constants.eqs.toString
case Operator.Lt => Constants.lt.toString
case Operator.Gt => Constants.gt.toString
case Operator.Neq => Constants.neq
}
private[internal] def tagOpStr(op: TagOperator): String =
op match {
case TagOperator.AllMatch => C.eqs.toString
case TagOperator.AnyMatch => C.like.toString
}
private def quote(s: String): String =
s"\"$s\""
}

View File

@ -13,35 +13,42 @@ import docspell.query.ItemQuery._
object ExprUtil { object ExprUtil {
def reduce(expr: Expr): Expr =
reduce(expandMacros = true)(expr)
/** Does some basic transformation, like unfolding nested and trees containing one value /** Does some basic transformation, like unfolding nested and trees containing one value
* etc. * etc.
*/ */
def reduce(expr: Expr): Expr = def reduce(expandMacros: Boolean)(expr: Expr): Expr =
expr match { expr match {
case AndExpr(inner) => case AndExpr(inner) =>
val nodes = spliceAnd(inner) val nodes = spliceAnd(inner)
if (nodes.tail.isEmpty) reduce(nodes.head) if (nodes.tail.isEmpty) reduce(expandMacros)(nodes.head)
else AndExpr(nodes.map(reduce)) else AndExpr(nodes.map(reduce(expandMacros)))
case OrExpr(inner) => case OrExpr(inner) =>
val nodes = spliceOr(inner) val nodes = spliceOr(inner)
if (nodes.tail.isEmpty) reduce(nodes.head) if (nodes.tail.isEmpty) reduce(expandMacros)(nodes.head)
else OrExpr(nodes.map(reduce)) else OrExpr(nodes.map(reduce(expandMacros)))
case NotExpr(inner) => case NotExpr(inner) =>
inner match { inner match {
case NotExpr(inner2) => case NotExpr(inner2) =>
reduce(inner2) reduce(expandMacros)(inner2)
case InboxExpr(flag) => case InboxExpr(flag) =>
InboxExpr(!flag) InboxExpr(!flag)
case DirectionExpr(flag) => case DirectionExpr(flag) =>
DirectionExpr(!flag) DirectionExpr(!flag)
case _ => case _ =>
NotExpr(reduce(inner)) NotExpr(reduce(expandMacros)(inner))
} }
case m: MacroExpr => case m: MacroExpr =>
reduce(m.body) if (expandMacros) {
reduce(expandMacros)(m.body)
} else {
m
}
case DirectionExpr(_) => case DirectionExpr(_) =>
expr expr

View File

@ -0,0 +1,287 @@
/*
* Copyright 2020 Eike K. & Contributors
*
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
package docspell.query
import java.time.{Instant, Period, ZoneOffset}
import cats.data.NonEmptyList
import docspell.query.ItemQuery.Expr.TagIdsMatch
import docspell.query.ItemQuery._
import org.scalacheck.Gen
/** Generator for syntactically valid queries. */
object ItemQueryGen {
def exprGen: Gen[Expr] =
Gen.oneOf(
simpleExprGen,
existsExprGen,
inExprGen,
inDateExprGen,
inboxExprGen,
directionExprGen,
tagIdsMatchExprGen,
tagMatchExprGen,
tagCatMatchExpr,
customFieldMatchExprGen,
customFieldIdMatchExprGen,
fulltextExprGen,
checksumMatchExprGen,
attachIdExprGen,
namesMacroGen,
corrMacroGen,
concMacroGen,
yearMacroGen,
dateRangeMacro,
Gen.lzy(andExprGen(exprGen)),
Gen.lzy(orExprGen(exprGen)),
Gen.lzy(notExprGen(exprGen))
)
def andExprGen(g: Gen[Expr]): Gen[Expr.AndExpr] =
nelGen(g).map(Expr.AndExpr)
def orExprGen(g: Gen[Expr]): Gen[Expr.OrExpr] =
nelGen(g).map(Expr.OrExpr)
// avoid generating nested not expressions, they are already flattened by the parser
// and only occur artificially
def notExprGen(g: Gen[Expr]): Gen[Expr] =
g.map {
case Expr.NotExpr(inner) => inner
case e => Expr.NotExpr(e)
}
val opGen: Gen[Operator] =
Gen.oneOf(
Operator.Like,
Operator.Gte,
Operator.Lt,
Operator.Gt,
Operator.Lte,
Operator.Eq,
Operator.Neq
)
val tagOpGen: Gen[TagOperator] =
Gen.oneOf(TagOperator.AllMatch, TagOperator.AnyMatch)
val stringAttrGen: Gen[Attr.StringAttr] =
Gen.oneOf(
Attr.Concerning.EquipName,
Attr.Concerning.EquipId,
Attr.Concerning.PersonName,
Attr.Concerning.PersonId,
Attr.Correspondent.OrgName,
Attr.Correspondent.OrgId,
Attr.Correspondent.PersonName,
Attr.Correspondent.PersonId,
Attr.ItemId,
Attr.ItemName,
Attr.ItemSource,
Attr.ItemNotes,
Attr.Folder.FolderId,
Attr.Folder.FolderName
)
val dateAttrGen: Gen[Attr.DateAttr] =
Gen.oneOf(Attr.Date, Attr.DueDate, Attr.CreatedDate)
val intAttrGen: Gen[Attr.IntAttr] =
Gen.const(Attr.AttachCount)
val attrGen: Gen[Attr] =
Gen.oneOf(stringAttrGen, dateAttrGen, intAttrGen)
private val valueChars =
Gen.oneOf(Gen.alphaNumChar, Gen.oneOf(" /{}*?-:@#$~+%…_[]^!ß"))
private val stringValueGen: Gen[String] =
Gen.choose(1, 20).flatMap(n => Gen.stringOfN(n, valueChars))
private val intValueGen: Gen[Int] =
Gen.choose(1900, 9999)
private val identGen: Gen[String] =
Gen
.choose(3, 12)
.flatMap(n =>
Gen.stringOfN(
n,
Gen.oneOf((('A' to 'Z') ++ ('a' to 'z') ++ ('0' to '9') ++ "-_.@").toSet)
)
)
private def nelGen[T](gen: Gen[T]): Gen[NonEmptyList[T]] =
for {
head <- gen
tail <- Gen.choose(0, 9).flatMap(n => Gen.listOfN(n, gen))
} yield NonEmptyList(head, tail)
private val dateMillisGen: Gen[Long] =
Gen.choose(0, Instant.parse("2100-12-24T20:00:00Z").toEpochMilli)
val localDateGen: Gen[Date.Local] =
dateMillisGen
.map(ms => Instant.ofEpochMilli(ms).atOffset(ZoneOffset.UTC).toLocalDate)
.map(Date.Local)
val millisDateGen: Gen[Date.Millis] =
dateMillisGen.map(Date.Millis)
val dateLiteralGen: Gen[Date.DateLiteral] =
Gen.oneOf(
localDateGen,
millisDateGen,
Gen.const(Date.Today)
)
val periodGen: Gen[Period] =
for {
mOrD <- Gen.oneOf(a => Period.ofDays(a), a => Period.ofMonths(a))
num <- Gen.choose(1, 30)
} yield mOrD(num)
val calcGen: Gen[Date.CalcDirection] =
Gen.oneOf(Date.CalcDirection.Plus, Date.CalcDirection.Minus)
val dateCalcGen: Gen[Date.Calc] =
for {
dl <- dateLiteralGen
calc <- calcGen
period <- periodGen
} yield Date.Calc(dl, calc, period)
val dateValueGen: Gen[Date] =
Gen.oneOf(dateLiteralGen, dateCalcGen)
val stringPropGen: Gen[Property.StringProperty] =
for {
attr <- stringAttrGen
sval <- stringValueGen
} yield Property.StringProperty(attr, sval)
val intPropGen: Gen[Property.IntProperty] =
for {
attr <- intAttrGen
ival <- intValueGen
} yield Property.IntProperty(attr, ival)
val datePropGen: Gen[Property.DateProperty] =
for {
attr <- dateAttrGen
dv <- dateValueGen
} yield Property.DateProperty(attr, dv)
val propertyGen: Gen[Property] =
Gen.oneOf(stringPropGen, datePropGen, intPropGen)
val simpleExprGen: Gen[Expr.SimpleExpr] =
for {
op <- opGen
prop <- propertyGen
} yield Expr.SimpleExpr(op, prop)
val existsExprGen: Gen[Expr.Exists] =
attrGen.map(Expr.Exists)
val inExprGen: Gen[Expr.InExpr] =
for {
attr <- stringAttrGen
vals <- nelGen(stringValueGen)
} yield Expr.InExpr(attr, vals)
val inDateExprGen: Gen[Expr.InDateExpr] =
for {
attr <- dateAttrGen
vals <- nelGen(dateValueGen)
} yield Expr.InDateExpr(attr, vals)
val inboxExprGen: Gen[Expr.InboxExpr] =
Gen.oneOf(true, false).map(Expr.InboxExpr)
val directionExprGen: Gen[Expr.DirectionExpr] =
Gen.oneOf(true, false).map(Expr.DirectionExpr)
val tagIdsMatchExprGen: Gen[Expr.TagIdsMatch] =
for {
op <- tagOpGen
vals <- nelGen(stringValueGen)
} yield TagIdsMatch(op, vals)
val tagMatchExprGen: Gen[Expr.TagsMatch] =
for {
op <- tagOpGen
vals <- nelGen(stringValueGen)
} yield Expr.TagsMatch(op, vals)
val tagCatMatchExpr: Gen[Expr.TagCategoryMatch] =
for {
op <- tagOpGen
vals <- nelGen(stringValueGen)
} yield Expr.TagCategoryMatch(op, vals)
val customFieldMatchExprGen: Gen[Expr.CustomFieldMatch] =
for {
name <- identGen
op <- opGen
value <- stringValueGen
} yield Expr.CustomFieldMatch(name, op, value)
val customFieldIdMatchExprGen: Gen[Expr.CustomFieldIdMatch] =
for {
name <- identGen
op <- opGen
value <- identGen
} yield Expr.CustomFieldIdMatch(name, op, value)
val fulltextExprGen: Gen[Expr.Fulltext] =
Gen
.choose(3, 20)
.flatMap(n => Gen.stringOfN(n, valueChars))
.map(Expr.Fulltext)
val checksumMatchExprGen: Gen[Expr.ChecksumMatch] =
Gen.stringOfN(64, Gen.hexChar).map(Expr.ChecksumMatch)
val attachIdExprGen: Gen[Expr.AttachId] =
identGen.map(Expr.AttachId)
val namesMacroGen: Gen[Expr.NamesMacro] =
stringValueGen.map(Expr.NamesMacro)
val concMacroGen: Gen[Expr.ConcMacro] =
stringValueGen.map(Expr.ConcMacro)
val corrMacroGen: Gen[Expr.CorrMacro] =
stringValueGen.map(Expr.CorrMacro)
val yearMacroGen: Gen[Expr.YearMacro] =
Gen.choose(1900, 9999).map(Expr.YearMacro(Attr.Date, _))
val dateRangeMacro: Gen[Expr.DateRangeMacro] =
for {
attr <- dateAttrGen
dl <- dateLiteralGen
p <- periodGen
calc <- Gen.option(calcGen)
range = calc match {
case Some(c @ Date.CalcDirection.Plus) =>
Expr.DateRangeMacro(attr, dl, Date.Calc(dl, c, p))
case Some(c @ Date.CalcDirection.Minus) =>
Expr.DateRangeMacro(attr, Date.Calc(dl, c, p), dl)
case None =>
Expr.DateRangeMacro(
attr,
Date.Calc(dl, Date.CalcDirection.Minus, p),
Date.Calc(dl, Date.CalcDirection.Plus, p)
)
}
} yield range
}

View File

@ -0,0 +1,69 @@
/*
* Copyright 2020 Eike K. & Contributors
*
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
package docspell.query.internal
import java.time.{LocalDate, Period}
import docspell.query.ItemQuery._
import docspell.query.{Date, ItemQueryGen, ParseFailure}
import munit.{FunSuite, ScalaCheckSuite}
import org.scalacheck.Prop.forAll
class ExprStringTest extends FunSuite with ScalaCheckSuite {
// parses the query without reducing and expanding macros
def singleParse(s: String): Expr =
ExprParser
.parseQuery(s)
.left
.map(ParseFailure.fromError(s))
.fold(f => sys.error(f.render), _.expr)
def exprString(expr: Expr): String =
ExprString(expr).fold(f => sys.error(f.toString), identity)
test("macro: name") {
val str = exprString(Expr.NamesMacro("test"))
val q = singleParse(str)
assertEquals(str, "names:\"test\"")
assertEquals(q, Expr.NamesMacro("test"))
}
test("macro: year") {
val str = exprString(Expr.YearMacro(Attr.Date, 1990))
val q = singleParse(str)
assertEquals(str, "year:1990")
assertEquals(q, Expr.YearMacro(Attr.Date, 1990))
}
test("macro: daterange") {
val range = Expr.DateRangeMacro(
attr = Attr.Date,
left = Date.Calc(
date = Date.Local(
date = LocalDate.of(2076, 12, 9)
),
calc = Date.CalcDirection.Minus,
period = Period.ofMonths(27)
),
right = Date.Local(LocalDate.of(2076, 12, 9))
)
val str = exprString(range)
val q = singleParse(str)
assertEquals(str, "dateIn:2076-12-09;-27m")
assertEquals(q, range)
}
property("generate expr and parse it") {
forAll(ItemQueryGen.exprGen) { expr =>
val str = exprString(expr)
val q = singleParse(str)
assertEquals(q, expr)
}
}
}

View File

@ -8,7 +8,7 @@ package docspell.query.internal
import cats.implicits._ import cats.implicits._
import docspell.query.ItemQueryParser import docspell.query.{ItemQuery, ItemQueryParser}
import munit._ import munit._
@ -64,4 +64,14 @@ class ItemQueryParserTest extends FunSuite {
ItemQueryParser.parseUnsafe("(| name:hello date:2021-02 name:world name:hello )") ItemQueryParser.parseUnsafe("(| name:hello date:2021-02 name:world name:hello )")
assertEquals(expect.copy(raw = raw.some), q) assertEquals(expect.copy(raw = raw.some), q)
} }
test("f.id:name=value") {
val raw = "f.id:QsuGW@=\"dAHBstXJd0\""
val q = ItemQueryParser.parseUnsafe(raw)
val expect =
ItemQuery.Expr.CustomFieldIdMatch("QsuGW@", ItemQuery.Operator.Eq, "dAHBstXJd0")
assertEquals(q.expr, expect)
}
} }

View File

@ -11,6 +11,7 @@ import java.time.{Instant, LocalDate}
import docspell.common._ import docspell.common._
import docspell.common.syntax.all._ import docspell.common.syntax.all._
import docspell.query.{ItemQuery, ItemQueryParser}
import docspell.totp.Key import docspell.totp.Key
import com.github.eikek.calev.CalEvent import com.github.eikek.calev.CalEvent
@ -142,6 +143,11 @@ trait DoobieMeta extends EmilDoobieMeta {
implicit val metaByteSize: Meta[ByteSize] = implicit val metaByteSize: Meta[ByteSize] =
Meta[Long].timap(ByteSize.apply)(_.bytes) Meta[Long].timap(ByteSize.apply)(_.bytes)
implicit val metaItemQuery: Meta[ItemQuery] =
Meta[String].timap(s => ItemQueryParser.parseUnsafe(s))(q =>
q.raw.getOrElse(ItemQueryParser.unsafeAsString(q.expr))
)
} }
object DoobieMeta extends DoobieMeta { object DoobieMeta extends DoobieMeta {

View File

@ -0,0 +1,59 @@
/*
* Copyright 2020 Eike K. & Contributors
*
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
package docspell.store.records
import cats.data.NonEmptyList
import docspell.common._
import docspell.query.ItemQuery
import docspell.store.qb._
final case class RShare(
id: Ident,
cid: Ident,
query: ItemQuery,
enabled: Boolean,
password: Option[Password],
publishedAt: Timestamp,
publishedUntil: Timestamp,
views: Int,
lastAccess: Option[Timestamp]
) {}
object RShare {
final case class Table(alias: Option[String]) extends TableDef {
val tableName = "item_share";
val id = Column[Ident]("id", this)
val cid = Column[Ident]("cid", this)
val query = Column[ItemQuery]("query", this)
val enabled = Column[Boolean]("enabled", this)
val password = Column[Password]("password", this)
val publishedAt = Column[Timestamp]("published_at", this)
val publishedUntil = Column[Timestamp]("published_until", this)
val views = Column[Int]("views", this)
val lastAccess = Column[Timestamp]("last_access", this)
val all: NonEmptyList[Column[_]] =
NonEmptyList.of(
id,
cid,
query,
enabled,
password,
publishedAt,
publishedUntil,
views,
lastAccess
)
}
val T: Table = Table(None)
def as(alias: String): Table = Table(Some(alias))
}