Skip to content

custom field rest endpoints #3038

@greshny

Description

@greshny

Hi

I have a question about API for custom fields, API doesn't return anything and I can't understand why

There is a scala-cli script (project.scala):

//> using scala 3.6.4
//> using toolkit typelevel:0.1.29
//> using dep org.typelevel::log4cats-core:2.7.0
//> using dep org.typelevel::log4cats-slf4j:2.7.0
//> using dep ch.qos.logback:logback-classic:1.5.18

import cats.effect.kernel.Async
import cats.effect.{ExitCode, IO}
import cats.syntax.all.*
import com.monovore.decline.Opts
import com.monovore.decline.effect.CommandIOApp
import io.circe.syntax.*
import io.circe.{Decoder, Encoder, Json, JsonObject}
import org.http4s.circe.*
import org.http4s.circe.CirceEntityCodec.*
import org.http4s.client.Client
import org.http4s.client.middleware.Logger as LoggerMiddleware
import org.http4s.ember.client.EmberClientBuilder
import org.http4s.headers.Accept
import org.http4s.{
  EntityEncoder,
  Header,
  Headers,
  MediaType,
  Method,
  Request,
  Uri
}
import org.typelevel.ci.CIString
import org.typelevel.log4cats.slf4j.Slf4jFactory
import org.typelevel.log4cats.{Logger, LoggerFactory}

trait Auth[F[_]]:
  def login(host: String, user: String, password: String): F[String]

object Auth:
  def make[F[_]: Logger: Async](client: Client[F]): Auth[F] = new Auth[F]:
    override def login(
        host: String,
        user: String,
        password: String
    ): F[String] =
      val body = JsonObject(
        "account" -> user.asJson,
        "password" -> password.asJson,
        "rememberMe" -> true.asJson
      ).asJson

      val req = Request[F](
        method = Method.POST,
        uri = Uri
          .unsafeFromString(s"https://$host")
          .withPath("/api/v1/open/auth/login"),
        headers = Headers(Accept(MediaType.application.json))
      ).withEntity(body)

      client
        .expect[Json](req)(jsonOf[F, Json])
        .flatTap: json =>
          Logger[F].info(s"got response $json")
        .flatMap: json =>
          Async[F].fromEither(
            json.hcursor.downField("token").as[String]
          )

trait RestClient[F[_]]:
  def get[A: Decoder](path: String): F[A]
  def post[A: Decoder, B: Encoder: Decoder](path: String, body: B)(using
      EntityEncoder[F, B]
  ): F[A]

object RestClient:
  def make[F[_]: Logger: Async](
      client: Client[F],
      token: String,
      host: String
  ): RestClient[F] = new RestClient[F]:
    override def get[A: Decoder](path: String): F[A] =
      client.expect(mkRequest(path, Method.GET))(jsonOf[F, A])

    override def post[A: Decoder, B: Encoder: Decoder](path: String, body: B)(
        using EntityEncoder[F, B]
    ): F[A] =
      client.expect(mkRequest(path, Method.POST).withEntity(body.asJson))(
        jsonOf[F, A]
      )

    private def mkRequest(path: String, method: Method): Request[F] =
      Request[F](
        method = method,
        uri = Uri
          .unsafeFromString(s"https://$host")
          .withPath(path),
        headers = Headers(
          Header.Raw(CIString("X-Docspell-Auth"), token),
          Accept(MediaType.application.json)
        )
      )

object MainApp
    extends CommandIOApp(
      name = "customfields",
      header = "test customfields api",
      helpFlag = true,
      version = "0.0.0"
    ):
  override def main: Opts[IO[ExitCode]] =
    given LoggerFactory[IO] = Slf4jFactory.create[IO]
    given Logger[IO] = LoggerFactory[IO].getLogger

    val hostOpt =
      Opts.option[String]("host", help = "Docspell host")

    val userOpt =
      Opts.option[String]("user", help = "Docspell username")

    val passwordOpt =
      Opts.option[String]("password", help = "Docspell password")

    (hostOpt, userOpt, passwordOpt).mapN:
      case (host, user, password) =>
        EmberClientBuilder
          .default[IO]
          .build
          .map: client =>
            LoggerMiddleware[IO](
              logHeaders = true,
              logBody = true,
              logAction = Some((msg: String) => Logger[IO].info(msg))
            )(client)
          .use: client =>
            val auth = Auth.make[IO](client = client)

            for
              token <- auth.login(host = host, user = user, password = password)
              rest = RestClient
                .make[IO](token = token, host = host, client = client)
              _ <- rest.post[Json, Json](
                "/api/v1/sec/customfield",
                JsonObject(
                  "name" -> Json.fromString("asn"),
                  "ftype" -> Json.fromString("text"),
                  "label" -> Json.fromString("ASN")
                ).asJson
              )
              _ <- rest.get[Json]("/api/v1/sec/customfield?q=asn")
            yield ExitCode.Success

When I run it:

scala run project.scala -- --host myhost.com --user mycollective/myuser --password mypassword

I receive an empty collection for custom fields "{"items":[]}":

11:44:29.405 [io-compute-3] INFO <empty>.MainApp -- HTTP/1.1 POST https://myhost.com/api/v1/open/auth/login Headers(Content-Length: 56, Accept: application/json, Content-Type: application/json, Accept: application/json) body="{"account":"mycolletive/myuser","password":"mypassword","rememberMe":true}"
11:44:29.534 [io-compute-3] INFO <empty>.MainApp -- HTTP/1.1 200 OK Headers(Server: nginx, Date: Wed, 07 May 2025 09:44:29 GMT, Content-Type: application/json, Content-Length: 294, Connection: keep-alive, Keep-Alive: timeout=20, Set-Cookie: <REDACTED>, Set-Cookie: <REDACTED>) body="{"collective":"mycollective","user":"myuser","success":true,"message":"Login successful","token":"mytoken","validMs":300000,"requireSecondFactor":false}"
11:44:29.535 [io-compute-3] INFO <empty>.MainApp -- got response {
  "collective" : "mycollective",
  "user" : "myuser",
  "success" : true,
  "message" : "Login successful",
  "token" : "mytoken",
  "validMs" : 300000,
  "requireSecondFactor" : false
}
11:44:29.541 [io-compute-6] INFO <empty>.MainApp -- HTTP/1.1 POST https://myhost.com/api/v1/sec/customfield Headers(Content-Length: 43, X-Docspell-Auth: mytoken, Accept: application/json, Content-Type: application/json, Accept: application/json) body="{"name":"asn","ftype":"text","label":"ASN"}"
11:44:29.573 [io-compute-5] INFO <empty>.MainApp -- HTTP/1.1 200 OK Headers(Server: nginx, Date: Wed, 07 May 2025 09:44:29 GMT, Content-Type: application/json, Content-Length: 47, Connection: keep-alive, Keep-Alive: timeout=20) body="{"success":true,"message":"New field created."}"
11:44:29.577 [io-compute-6] INFO <empty>.MainApp -- HTTP/1.1 GET https://myhost.com/api/v1/sec/customfield?q=asn Headers(X-Docspell-Auth: mytoken, Accept application/json, Accept: application/json) body=""
11:44:29.598 [io-compute-2] INFO <empty>.MainApp -- HTTP/1.1 200 OK Headers(Server: nginx, Date: Wed, 07 May 2025 09:44:29 GMT, Content-Type: application/json, Content-Length: 12, Connection: keep-alive, Keep-Alive: timeout=20) body="{"items":[]}"

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions