eikek / eikek/docspell

custom field rest endpoints

Open
#3,038 2 comments 0 reactions 0 assignees View on GitHub
Dominant language
Elm
Stars
2.3k
Forks
184
Avg merge
8h 2m
Merged PRs (30d)
4

Description

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`):

```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:

```sh
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 .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 .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: , Set-Cookie: ) body="{"collective":"mycollective","user":"myuser","success":true,"message":"Login successful","token":"mytoken","validMs":300000,"requireSecondFactor":false}"
11:44:29.535 [io-compute-3] INFO .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 .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 .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 .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 .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":[]}"

```

Contributor guide

Open the contributing guide

Research direction

Start with the reproduction in project.scala and run the Scala CLI script against the documented custom-field endpoints. Compare the successful POST response with the subsequent GET response and trace the custom-field API implementation to determine whether the query, scope, or response behavior is responsible. Done means the cause and expected API behavior are established.

Written by the indexing model from the issue text.

Assessment

Tech stack
scala
Domain
api, backend
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Stale
Clarity
Needs clarification
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.