apollographql / apollo-kotlin-execution

GraphQL execution algorithms
https://apollographql.github.io/apollo-kotlin-execution/
MIT License
3 stars 2 forks source link

Support for persisted queries #16

Closed brezinajn closed 3 months ago

brezinajn commented 3 months ago

Currently there is no way to hook in an adapter to handle persisted queries. If this issue is out of scope of intended features, please feel free to close it.

martinbonnin commented 3 months ago

Hi 👋 . This repo is only about an HttpEngine implementation. Apollo Kotlin supports persisted queries. See https://www.apollographql.com/docs/kotlin/advanced/persisted-queries for more details.

brezinajn commented 3 months ago

Hi, I'm not sure if we understand each other. My issue with the HttpEngine integration is there is no way to customize in this case ExecutableSchema::getOperationExecutor to implement persisted queries functionality.

martinbonnin commented 3 months ago

I just transferred the issue to https://github.com/apollographql/apollo-kotlin-execution/ but I'm actually not sure if this is the good place. Are you building a client or a server?

brezinajn commented 3 months ago

A server. Since Application::apolloModule lives in com.apollographql.execution.ktor this looks like it might be the right place. Thank you

martinbonnin commented 3 months ago

Nice! We're in the right spot 👍

I think you should be able to add persisted queries using ExecutableSchema.Builder.persistedDocumentCache():

    val executableSchema = MyExecutableSchemaBuilder()
        .persistedDocumentCache(InMemoryPersistedDocumentCache())
        .build()

    embeddedServer(Netty, port = 8080, host = "0.0.0.0") {
      apolloModule(executableSchema)
    }.start(wait = true)

Heads up that this whole repo is in its very early days so use at your own risks

brezinajn commented 3 months ago

You are right. Thank you. Works fine.

Yeah, I'm aware this is an early implementation. Being fan of Kotlin Apollo client, I'm just paying around, looking for limitations. My toughts so far:

Thank you for your time 👍 Closing the issue 🙂

martinbonnin commented 3 months ago

Not having access to ApplicationCall (ktor specific) makes authentication not really viable.

I think you can have global authentication but maybe not per-endpoint? (https://ktor.io/docs/server-auth.html#configure)

Do not hesitate to share the API of what you'd like to see, or even a pull request.

brezinajn commented 3 months ago

That's not the issue. The issue is not having a call context in the resolver. Let's say I want to enable user to edit his own username (and of course not username of anyone else)

Quick, single file demonstration

package com.example

import com.apollographql.execution.annotation.GraphQLQueryRoot
import com.apollographql.execution.ktor.respondGraphQL
import io.ktor.server.application.*
import io.ktor.server.auth.*
import io.ktor.server.engine.*
import io.ktor.server.netty.*
import io.ktor.server.routing.*

fun main() {
    embeddedServer(Netty, port = 8080, host = "0.0.0.0", module = Application::module)
        .start(wait = true)
}

fun Application.module() {
    val executableSchema = ServiceExecutableSchemaBuilder().build()

    authentication {
        basic(name = "myauth") {
            validate { credentials ->
                getUserByCredentials(credentials)?.let { UserIdPrincipal(it.id) }
            }
        }
    }

    routing {
        authenticate("myauth") {
            post("graphql") {
                call.respondGraphQL(executableSchema)
            }
        }
    }
}

@GraphQLQueryRoot
class Query {
    /**
     * Updates own username
     */
    fun ApplicationCall.updateOwnUsername(name: String) {
        val userId = principal<UserIdPrincipal>()!!.id

        updateUsernameById(userId, name)
    }
}

data class UserIdPrincipal(val id: Int) : Principal

data class User(val id: Int, val name: String)

fun getUserByCredentials(credentials: UserPasswordCredential): User? {
    TODO()
}

fun updateUsernameById(id: Int, name: String) {
    TODO()
}

The thing is, without updateOwnUsername having an access to the context of the call, I'm unable to access the principal, so I can't get info on who's making the request.

If we agree on a way to proceed, I can cook up a PR.

I think the main question is, how to approach the problem of providing context in a server agnostic way, to support both http4k and Ktor.

martinbonnin commented 3 months ago

You can add an ExecutionContext parameter as a first parameter to any GraphQL function for that purpose.

ExecutionContext is a CoroutineContext-like structure that you can use to tunnel any kind of context into the resolver functions. In that case, you can probably tunnel the call in there. And in order to get access to the call, I think you should be able to use lower level APIs like call.respondGraphQL(executableSchema).

All in all, something like this (pseudo code completely non-tested):

class CallExecutionContext(val currentCall: Call) :  ExecutionContext.Element {

  override val key: ExecutionContext.Key<CallExecutionContext> = Key

  companion object Key : ExecutionContext.Key<CallExecutionContext>
}

fun Application.myApolloModule(
    executableSchema: ExecutableSchema,
    path: String = "/graphql",
) {
  routing {
    post(path) {
      call.respondGraphQL(executableSchema, CallExecutionContext(call))
    }
  }
}

Maybe that execution context could be made available by default, ~that sounds reasonnable~ edit: I'm not sure actually, maybe some users would prefer passing their own custom authentication information there...

Again, apologies there are no docs for this but contributions are definitely welcome!

brezinajn commented 3 months ago

Works like a charm. You're probably right. It might be better to let users choose level of abstraction that fits their needs..