mirror of
https://github.com/TheAnachronism/docspell.git
synced 2025-06-06 07:05:59 +00:00
Prepare custom fields
This commit is contained in:
parent
0f45e1b097
commit
248ad04dd0
@ -183,6 +183,8 @@ val openapiScalaSettings = Seq(
|
|||||||
)
|
)
|
||||||
case "glob" =>
|
case "glob" =>
|
||||||
field => field.copy(typeDef = TypeDef("Glob", Imports("docspell.common.Glob")))
|
field => field.copy(typeDef = TypeDef("Glob", Imports("docspell.common.Glob")))
|
||||||
|
case "customfieldtype" =>
|
||||||
|
field => field.copy(typeDef = TypeDef("CustomFieldType", Imports("docspell.common.CustomFieldType")))
|
||||||
}))
|
}))
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -36,6 +36,7 @@ trait BackendApp[F[_]] {
|
|||||||
def joex: OJoex[F]
|
def joex: OJoex[F]
|
||||||
def userTask: OUserTask[F]
|
def userTask: OUserTask[F]
|
||||||
def folder: OFolder[F]
|
def folder: OFolder[F]
|
||||||
|
def customFields: OCustomFields[F]
|
||||||
}
|
}
|
||||||
|
|
||||||
object BackendApp {
|
object BackendApp {
|
||||||
@ -66,27 +67,29 @@ object BackendApp {
|
|||||||
fulltextImpl <- OFulltext(itemSearchImpl, ftsClient, store, queue, joexImpl)
|
fulltextImpl <- OFulltext(itemSearchImpl, ftsClient, store, queue, joexImpl)
|
||||||
javaEmil =
|
javaEmil =
|
||||||
JavaMailEmil(blocker, Settings.defaultSettings.copy(debug = cfg.mailDebug))
|
JavaMailEmil(blocker, Settings.defaultSettings.copy(debug = cfg.mailDebug))
|
||||||
mailImpl <- OMail(store, javaEmil)
|
mailImpl <- OMail(store, javaEmil)
|
||||||
userTaskImpl <- OUserTask(utStore, queue, joexImpl)
|
userTaskImpl <- OUserTask(utStore, queue, joexImpl)
|
||||||
folderImpl <- OFolder(store)
|
folderImpl <- OFolder(store)
|
||||||
|
customFieldsImpl <- OCustomFields(store)
|
||||||
} yield new BackendApp[F] {
|
} yield new BackendApp[F] {
|
||||||
val login: Login[F] = loginImpl
|
val login = loginImpl
|
||||||
val signup: OSignup[F] = signupImpl
|
val signup = signupImpl
|
||||||
val collective: OCollective[F] = collImpl
|
val collective = collImpl
|
||||||
val source = sourceImpl
|
val source = sourceImpl
|
||||||
val tag = tagImpl
|
val tag = tagImpl
|
||||||
val equipment = equipImpl
|
val equipment = equipImpl
|
||||||
val organization = orgImpl
|
val organization = orgImpl
|
||||||
val upload = uploadImpl
|
val upload = uploadImpl
|
||||||
val node = nodeImpl
|
val node = nodeImpl
|
||||||
val job = jobImpl
|
val job = jobImpl
|
||||||
val item = itemImpl
|
val item = itemImpl
|
||||||
val itemSearch = itemSearchImpl
|
val itemSearch = itemSearchImpl
|
||||||
val fulltext = fulltextImpl
|
val fulltext = fulltextImpl
|
||||||
val mail = mailImpl
|
val mail = mailImpl
|
||||||
val joex = joexImpl
|
val joex = joexImpl
|
||||||
val userTask = userTaskImpl
|
val userTask = userTaskImpl
|
||||||
val folder = folderImpl
|
val folder = folderImpl
|
||||||
|
val customFields = customFieldsImpl
|
||||||
}
|
}
|
||||||
|
|
||||||
def apply[F[_]: ConcurrentEffect: ContextShift](
|
def apply[F[_]: ConcurrentEffect: ContextShift](
|
||||||
|
@ -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))
|
||||||
|
})
|
||||||
|
}
|
@ -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)
|
||||||
|
}
|
@ -3202,6 +3202,23 @@ paths:
|
|||||||
schema:
|
schema:
|
||||||
$ref: "#/components/schemas/BasicResult"
|
$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:
|
components:
|
||||||
schemas:
|
schemas:
|
||||||
ItemsAndRefs:
|
ItemsAndRefs:
|
||||||
@ -3282,6 +3299,38 @@ components:
|
|||||||
format: date-time
|
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:
|
JobPriority:
|
||||||
description: |
|
description: |
|
||||||
Transfer the priority of a job.
|
Transfer the priority of a job.
|
||||||
@ -4372,6 +4421,7 @@ components:
|
|||||||
type: array
|
type: array
|
||||||
items:
|
items:
|
||||||
$ref: "#/components/schemas/SourceAndTags"
|
$ref: "#/components/schemas/SourceAndTags"
|
||||||
|
|
||||||
Source:
|
Source:
|
||||||
description: |
|
description: |
|
||||||
Data about a Source. A source defines the endpoint where
|
Data about a Source. A source defines the endpoint where
|
||||||
|
@ -85,7 +85,8 @@ object RestServer {
|
|||||||
"usertask/scanmailbox" -> ScanMailboxRoutes(restApp.backend, token),
|
"usertask/scanmailbox" -> ScanMailboxRoutes(restApp.backend, token),
|
||||||
"calevent/check" -> CalEventCheckRoutes(),
|
"calevent/check" -> CalEventCheckRoutes(),
|
||||||
"fts" -> FullTextIndexRoutes.secured(cfg, restApp.backend, token),
|
"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] =
|
def openRoutes[F[_]: Effect](cfg: Config, restApp: RestApp[F]): HttpRoutes[F] =
|
||||||
|
@ -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)
|
||||||
|
}
|
@ -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")
|
||||||
|
)
|
@ -94,6 +94,9 @@ trait DoobieMeta extends EmilDoobieMeta {
|
|||||||
|
|
||||||
implicit val metaGlob: Meta[Glob] =
|
implicit val metaGlob: Meta[Glob] =
|
||||||
Meta[String].timap(Glob.apply)(_.asString)
|
Meta[String].timap(Glob.apply)(_.asString)
|
||||||
|
|
||||||
|
implicit val metaCustomFieldType: Meta[CustomFieldType] =
|
||||||
|
Meta[String].timap(CustomFieldType.unsafe)(_.name)
|
||||||
}
|
}
|
||||||
|
|
||||||
object DoobieMeta extends DoobieMeta {
|
object DoobieMeta extends DoobieMeta {
|
||||||
|
@ -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]
|
||||||
|
}
|
@ -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
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user