Prepare custom fields

This commit is contained in:
Eike Kettner 2020-09-28 22:54:35 +02:00
parent 0f45e1b097
commit 248ad04dd0
11 changed files with 297 additions and 21 deletions

View File

@ -183,6 +183,8 @@ val openapiScalaSettings = Seq(
)
case "glob" =>
field => field.copy(typeDef = TypeDef("Glob", Imports("docspell.common.Glob")))
case "customfieldtype" =>
field => field.copy(typeDef = TypeDef("CustomFieldType", Imports("docspell.common.CustomFieldType")))
}))
)

View File

@ -36,6 +36,7 @@ trait BackendApp[F[_]] {
def joex: OJoex[F]
def userTask: OUserTask[F]
def folder: OFolder[F]
def customFields: OCustomFields[F]
}
object BackendApp {
@ -66,27 +67,29 @@ object BackendApp {
fulltextImpl <- OFulltext(itemSearchImpl, ftsClient, store, queue, joexImpl)
javaEmil =
JavaMailEmil(blocker, Settings.defaultSettings.copy(debug = cfg.mailDebug))
mailImpl <- OMail(store, javaEmil)
userTaskImpl <- OUserTask(utStore, queue, joexImpl)
folderImpl <- OFolder(store)
mailImpl <- OMail(store, javaEmil)
userTaskImpl <- OUserTask(utStore, queue, joexImpl)
folderImpl <- OFolder(store)
customFieldsImpl <- OCustomFields(store)
} yield new BackendApp[F] {
val login: Login[F] = loginImpl
val signup: OSignup[F] = signupImpl
val collective: OCollective[F] = collImpl
val source = sourceImpl
val tag = tagImpl
val equipment = equipImpl
val organization = orgImpl
val upload = uploadImpl
val node = nodeImpl
val job = jobImpl
val item = itemImpl
val itemSearch = itemSearchImpl
val fulltext = fulltextImpl
val mail = mailImpl
val joex = joexImpl
val userTask = userTaskImpl
val folder = folderImpl
val login = loginImpl
val signup = signupImpl
val collective = collImpl
val source = sourceImpl
val tag = tagImpl
val equipment = equipImpl
val organization = orgImpl
val upload = uploadImpl
val node = nodeImpl
val job = jobImpl
val item = itemImpl
val itemSearch = itemSearchImpl
val fulltext = fulltextImpl
val mail = mailImpl
val joex = joexImpl
val userTask = userTaskImpl
val folder = folderImpl
val customFields = customFieldsImpl
}
def apply[F[_]: ConcurrentEffect: ContextShift](

View File

@ -0,0 +1,25 @@
package docspell.backend.ops
import cats.effect.{Effect, Resource}
import docspell.common._
import docspell.store.Store
import docspell.store.records.RCustomField
trait OCustomFields[F[_]] {
def findAll(coll: Ident): F[Vector[RCustomField]]
}
object OCustomFields {
def apply[F[_]: Effect](
store: Store[F]
): Resource[F, OCustomFields[F]] =
Resource.pure[F, OCustomFields[F]](new OCustomFields[F] {
def findAll(coll: Ident): F[Vector[RCustomField]] =
store.transact(RCustomField.findAll(coll))
})
}

View File

@ -0,0 +1,51 @@
package docspell.common
import io.circe._
sealed trait CustomFieldType { self: Product =>
final def name: String =
self.productPrefix.toLowerCase()
}
object CustomFieldType {
case object Text extends CustomFieldType
case object Numeric extends CustomFieldType
case object Date extends CustomFieldType
case object Bool extends CustomFieldType
case object Money extends CustomFieldType
def text: CustomFieldType = Text
def numeric: CustomFieldType = Numeric
def date: CustomFieldType = Date
def bool: CustomFieldType = Bool
def money: CustomFieldType = Money
val all: List[CustomFieldType] = List(Text, Numeric, Date, Bool, Money)
def fromString(str: String): Either[String, CustomFieldType] =
str.toLowerCase match {
case "text" => Right(text)
case "numeric" => Right(numeric)
case "date" => Right(date)
case "bool" => Right(bool)
case "money" => Right(money)
case _ => Left(s"Unknown custom field: $str")
}
def unsafe(str: String): CustomFieldType =
fromString(str).fold(sys.error, identity)
implicit val jsonDecoder: Decoder[CustomFieldType] =
Decoder.decodeString.emap(fromString)
implicit val jsonEncoder: Encoder[CustomFieldType] =
Encoder.encodeString.contramap(_.name)
}

View File

@ -3202,6 +3202,23 @@ paths:
schema:
$ref: "#/components/schemas/BasicResult"
/sec/customfields:
get:
tags: [ Custom Fields ]
summary: Get all defined custom fields.
description: |
Get all custom fields defined for the current collective.
security:
- authTokenHeader: []
responses:
200:
description: Ok
content:
application/json:
schema:
$ref: "#/components/schemas/CustomFieldList"
components:
schemas:
ItemsAndRefs:
@ -3282,6 +3299,38 @@ components:
format: date-time
CustomFieldList:
description: |
A list of known custom fields.
required:
- items
properties:
items:
type: array
items:
$ref: "#/components/schemas/CustomField"
CustomField:
description: |
A custom field definition.
required:
- id
- name
- ftype
- created
properties:
id:
type: string
format: ident
name:
type: string
ftype:
type: string
format: customfieldtype
created:
type: integer
format: date-time
JobPriority:
description: |
Transfer the priority of a job.
@ -4372,6 +4421,7 @@ components:
type: array
items:
$ref: "#/components/schemas/SourceAndTags"
Source:
description: |
Data about a Source. A source defines the endpoint where

View File

@ -85,7 +85,8 @@ object RestServer {
"usertask/scanmailbox" -> ScanMailboxRoutes(restApp.backend, token),
"calevent/check" -> CalEventCheckRoutes(),
"fts" -> FullTextIndexRoutes.secured(cfg, restApp.backend, token),
"folder" -> FolderRoutes(restApp.backend, token)
"folder" -> FolderRoutes(restApp.backend, token),
"customfield" -> CustomFieldRoutes(restApp.backend, token)
)
def openRoutes[F[_]: Effect](cfg: Config, restApp: RestApp[F]): HttpRoutes[F] =

View File

@ -0,0 +1,35 @@
package docspell.restserver.routes
import cats.effect._
import cats.implicits._
import docspell.backend.BackendApp
import docspell.backend.auth.AuthToken
import docspell.restapi.model._
import docspell.restserver.http4s._
import org.http4s.HttpRoutes
//import org.http4s.circe.CirceEntityDecoder._
import org.http4s.circe.CirceEntityEncoder._
import org.http4s.dsl.Http4sDsl
import docspell.store.records.RCustomField
object CustomFieldRoutes {
def apply[F[_]: Effect](backend: BackendApp[F], user: AuthToken): HttpRoutes[F] = {
val dsl = new Http4sDsl[F] with ResponseGenerator[F] {}
import dsl._
HttpRoutes.of {
case GET -> Root =>
for {
fs <- backend.customFields.findAll(user.account.collective)
res <- Ok(CustomFieldList(fs.map(convertField).toList))
} yield res
}
}
private def convertField(f: RCustomField): CustomField =
CustomField(f.id, f.name, f.ftype, f.created)
}

View File

@ -0,0 +1,20 @@
CREATE TABLE "custom_field" (
"id" varchar(254) not null primary key,
"name" varchar(254) not null,
"cid" varchar(254) not null,
"ftype" varchar(100) not null,
"created" timestamp not null,
foreign key ("cid") references "collective"("cid"),
unique ("cid", "name")
);
CREATE TABLE "custom_field_value" (
"id" varchar(254) not null primary key,
"item_id" varchar(254) not null,
"field" varchar(254) not null,
"value_text" varchar(300),
"value_numeric" numeric,
foreign key ("item_id") references "item"("itemid"),
foreign key ("field") references "custom_field"("id"),
unique ("item_id", "field")
)

View File

@ -94,6 +94,9 @@ trait DoobieMeta extends EmilDoobieMeta {
implicit val metaGlob: Meta[Glob] =
Meta[String].timap(Glob.apply)(_.asString)
implicit val metaCustomFieldType: Meta[CustomFieldType] =
Meta[String].timap(CustomFieldType.unsafe)(_.name)
}
object DoobieMeta extends DoobieMeta {

View File

@ -0,0 +1,44 @@
package docspell.store.records
import docspell.common._
import docspell.store.impl.Column
import docspell.store.impl.Implicits._
import doobie._
import doobie.implicits._
case class RCustomField(
id: Ident,
name: String,
cid: Ident,
ftype: CustomFieldType,
created: Timestamp
)
object RCustomField {
val table = fr"custom_field"
object Columns {
val id = Column("id")
val name = Column("name")
val cid = Column("cid")
val ftype = Column("ftype")
val created = Column("created")
val all = List(id, name, cid, ftype, created)
}
def insert(value: RCustomField): ConnectionIO[Int] = {
val sql = insertRow(
table,
Columns.all,
fr"${value.id},${value.name},${value.cid},${value.ftype},${value.created}"
)
sql.update.run
}
def findAll(coll: Ident): ConnectionIO[Vector[RCustomField]] =
selectSimple(Columns.all, table, Columns.cid.is(coll)).query[RCustomField].to[Vector]
}

View File

@ -0,0 +1,42 @@
package docspell.store.records
import docspell.common._
import docspell.store.impl.Column
import docspell.store.impl.Implicits._
import doobie._
import doobie.implicits._
case class RCustomFieldValue(
id: Ident,
itemId: Ident,
field: Ident,
valueText: Option[String],
valueNumeric: Option[BigDecimal]
)
object RCustomFieldValue {
val table = fr"custom_field_value"
object Columns {
val id = Column("id")
val itemId = Column("item_id")
val field = Column("field")
val valueText = Column("value_text")
val valueNumeric = Column("value_numeric")
val all = List(id, itemId, field, valueText, valueNumeric)
}
def insert(value: RCustomFieldValue): ConnectionIO[Int] = {
val sql = insertRow(
table,
Columns.all,
fr"${value.id},${value.itemId},${value.field},${value.valueText},${value.valueNumeric}"
)
sql.update.run
}
}