mirror of
				https://github.com/TheAnachronism/docspell.git
				synced 2025-11-03 18:00:11 +00:00 
			
		
		
		
	Prepare custom fields
This commit is contained in:
		@@ -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")))
 | 
			
		||||
    }))
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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 {
 | 
			
		||||
@@ -69,10 +70,11 @@ object BackendApp {
 | 
			
		||||
      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 login        = loginImpl
 | 
			
		||||
      val signup       = signupImpl
 | 
			
		||||
      val collective   = collImpl
 | 
			
		||||
      val source       = sourceImpl
 | 
			
		||||
      val tag          = tagImpl
 | 
			
		||||
      val equipment    = equipImpl
 | 
			
		||||
@@ -87,6 +89,7 @@ object BackendApp {
 | 
			
		||||
      val joex         = joexImpl
 | 
			
		||||
      val userTask     = userTaskImpl
 | 
			
		||||
      val folder       = folderImpl
 | 
			
		||||
      val customFields = customFieldsImpl
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
  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:
 | 
			
		||||
                $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
 | 
			
		||||
 
 | 
			
		||||
@@ -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] =
 | 
			
		||||
 
 | 
			
		||||
@@ -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] =
 | 
			
		||||
    Meta[String].timap(Glob.apply)(_.asString)
 | 
			
		||||
 | 
			
		||||
  implicit val metaCustomFieldType: Meta[CustomFieldType] =
 | 
			
		||||
    Meta[String].timap(CustomFieldType.unsafe)(_.name)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
		Reference in New Issue
	
	Block a user