Small demo. I created a small SSR web application ...
# http4k
c
Small demo. I created a small SSR web application PoC for using http4k with Supabase and Jdbi. It uses Supabase's authentication (JWT+refresh token), stored in cookies (so they can effectively be used in an SSR setup. I've decided to only use RLS for the reads (all writes are supposed to be blocked through RLS for the
authenticated
role, you simply need the
service_role
for writes); this to keep RLS dead simple and still useful (as an extra safety against data leaks and to allow Supabase goodness like GraphQL and realtime to be used later on). Both JWT-from-cookie and db-connection-with-supabase-auth are implemented as http4k `Filter`s. The idea is: start SSR, because it is just waaay simpler, then add little browser apps on top later where it adds maximum value (and for that Supabase is really nice with GraphQL and realtime). It used kotlinx.html for template rendering (with a layouting system) and Konform for FormDto validation. Jdbi was chosen as I want to be able to write "just SQL". I might one day convert it to terpal-sql (the basis of ExoQuery) but it did not work out of the box, so I went with time-tested Jdbi. Feel free to AMA, and if you have a look at the code and see any room for improvement: please let me know! I'm really hungry for feedback. https://github.com/cies/supabase-http4k-ssr-jdbi
d
nice! If you wanted to use typed lenses for the configuration then you can use http4k-config. Also - would maybe suggest Moshi over Jackson. It's much smaller. You could even use Kotshi KSP to generate adapters and get rid of reflection entirely 🙂
c
thanks for having a look. i'll look in to all of these. i wanted to use kotx.serialization, but found that it is very strict. this is nice "internally" but IMHO i rather have super lenient JSON deserializers when dealing with 3rd party's JSONs. I just knew that Jackson was able to do this an immediately reached for that. Also my "www-form-urlencoded form submisstions to FormDto" code (https://github.com/cies/supabase-http4k-ssr-jdbi/blob/master/src/main/kotlin/com/example/lib/formparser/FormParamsToJson.kt) was ripped out of another project and also uses Jackson (so I already incurred the dependency) for type coercion from the strings to the FormDto's types. But I'll look into the potential benefits Moshi in tat case as well, since you recommend it.
Just see the Jdbi kotlin integration also uses
kotlin-reflect
. Would like to use terpal-sql instead (which uses kotlinx.serialization), but I stumbled on errors trying it out, and it is sooo young that I dont feel very confident depending on it.
c
i'm now considering to write my "www-form-urlencoded form submit to FormDto" lib as a kotlinx.serialization library (only decode, not encode)... you sure stirred something up with your comments... Jackson and kotlin-reflect are huge dependencies indeed. I took them as fact-of-life, but now I envision a life without them 🙂
d
Jackson is legacy-ware really. it's bloated and often has security vulnerabilities.
c
did not know that... thanks for the heads-up.
Finally got
terpal-sql
to work, so no need for Jdbi (and
jdbi3-kotlin
which ropes in
kotlin-reflect
). Now see if I can make a deserializer for form data (with a naming scheme for the "names" or "keys" of the key-value pairs) to data objects with an @Serializable annotation.
d
for the form, we could do something like:
Copy code
data class Foo(val anInt: Int)
    val formLens = Body.autoWebForm().toLens()
    val foo = formLens(request)
although that wouldn't give you the fully featured validations. we could do something with delegates though 🤔
c
I've looked into this, and what I found is that all big webFWs have a naming scheme for the form
name
fields. We have standardized on the naming scheme of Konform (a validation lib for Kotlin that's quite nice and we also wanted to use). The naming scheme gives structure to the fields that are otherwise just a flat k/v list.
Copy code
The convention is simple: property names start with a dot (`.`) and array elements are
specified with a number between brackets (`[]`).
So this:
Copy code
.x.y[0].v.w[0]=1
would deserialize to this (if JSON):
Copy code
{
  "x": {
    "y": [
      {
        "v": {
          "w": [
            "1"
          ]
        }
      }
    ]
  }
}
This allows us to give form data structure and fill structured DTOs with it.
Copy code
data class RootFormDto(val x: YFormDto)
data class YFormDto(val y: List<VFormDto>)
data class VFormDto(val v: WFormDto)
data class WFormDto(val w: List<Int>)
We have quite complex forms at times. Being able to express them in www-form-urlencoded strings made a big difference.
I've just managed to get it to work with Moshi (and KSP generate)!!!
Copy code
@JsonClass(generateAdapter = true)
data class BookModel(
    val title: String,
    @Json(name = "page_count") val pageCount: Int,
    val genre: Genre,
    val author: Author
) {
    enum class Genre {
        FICTION,
        NONFICTION,
    }
    @JsonClass(generateAdapter = true)
    data class Author(val firstName: String?, val lastName: String?)
}

private val moshi = Moshi.Builder().build()
@OptIn(ExperimentalStdlibApi::class)
private val adapter = moshi.adapter<BookModel>()

fun main() {

    val book = adapter.fromJsonValue(
        mapOf(
            "title" to "a",
            "page_count" to "123",
            "genre" to "FICTION",
            "author" to mapOf("firstName" to "a", "lastName" to "b")
        )
    )
    println(book)
}
I'll go with this approach (Moshi) as writing a kotlinx.serializer that deserializes www-form-urlencoded to @Serializable-annotated data classes will be much harder and I want to spend my time else where. Thanks for your comments @dave!
managed to use kotx.serialization for all purposes. 🙂 whole stack now <5MB
feels good 🙂
Copy code
```
Configuration name: "implementationDependenciesMetadata"
Total dependencies size:                                               4.92 Mb

postgresql-42.7.3.jar                                              1,063.78 kb
kotlinx-html-jvm-0.11.0.jar                                          865.22 kb
http4k-core-6.9.2.0.jar                                              836.43 kb
okhttp-4.12.0.jar                                                    771.03 kb
kotlinx-serialization-json-jvm-1.8.1.jar                             270.48 kb
http4k-realtime-core-6.9.2.0.jar                                     198.30 kb
kotlin-stdlib-2.1.20-all.jar                                         188.35 kb
HikariCP-5.1.0.jar                                                   158.05 kb
kotlin-logging-jvm-7.0.7.jar                                         106.46 kb
http4k-format-core-6.9.2.0.jar                                        96.04 kb
slf4j-api-2.0.17.jar                                                  68.27 kb
kotlinx-html-metadata-0.11.0.jar                                      68.15 kb
http4k-config-6.9.2.0.jar                                             59.66 kb
http4k-format-kotlinx-serialization-6.9.2.0.jar                       56.52 kb
kotlinx-serialization-core-metadata-1.8.1.jar                         53.57 kb
kotlinx-serialization-json-metadata-1.8.1.jar                         42.05 kb
okio-metadata-3.6.0-all.jar                                           36.46 kb
http4k-client-okhttp-6.9.2.0.jar                                      28.90 kb
result4k-2.22.3.0.jar                                                 27.39 kb
konform-metadata-0.11.0.jar                                           19.92 kb
slf4j-simple-2.0.17.jar                                               15.35 kb
terpal-runtime-metadata-2.1.0-2.0.0.PL.jar                             6.83 kb
kotlin-stdlib-jdk8-1.8.21.jar                                          0.95 kb
kotlin-stdlib-jdk7-1.8.21.jar                                          0.94 kb
terpal-sql-jdbc-metadata-2.0.0.PL-1.2.0.jar                            0.63 kb
```
❤️ 1
ditched .env as well 😉
I've got the form deserialization down to this:
Copy code
fun signUpPostHandler(req: Request): Response {
  val formDto: SignUpForm = req.form().decodeOrFailWith { reason ->
    log.warn { "Failed to decode SignUpForm: $reason" }
    return Response(BAD_REQUEST)
  }

  when (val validationResult = formDto.validate()) {
    is Invalid -> return signUpPage(formDto, validationResult)

    is Valid<SignUpForm> -> {
// ...
With this begin the implementation of `deserializeOrFailWith`:
Copy code
inline fun <reified T> List<Pair<String, String?>>.decodeOrFailWith(block: (String) -> Nothing): T {
  val jsonElement = formToJsonElement(this).onFailure { block(it.reason) }
  try {
    // Maybe build in leniency + heavy warnings in some cases
    return strictJsonDecoder.decodeFromJsonElement<T>(jsonElement)
  } catch (e: Exception) {
    block(e.message ?: "Unknown deserialization error")
  }
}
Sweet.
There's 100LOC in
fun formToJsonElement(pairs: List<Pair<String, String?>>): Result<JsonElement, String> { ... }
, but i still have to make all tests pass there.