aPureBase / KGraphQL

Pure Kotlin GraphQL implementation
https://kgraphql.io
MIT License
298 stars 58 forks source link

Access data from resolvers from previous node #128

Open NathanPB opened 3 years ago

NathanPB commented 3 years ago

Hello, I don't know if I'm doing something wrong, but I'd like to access the data resolved in the previous nodes of an execution.

I'm sharing a schema to help demonstrate:

enum ContactVisibilty { PUBLIC, FRIENDS, PRIVATE }

type ContactField { 
  value: String!
  visibility: ContactVisibility!
}

type UserContact {
  discord: ContactField
  skype: ContactField
}

type UserProfile {
  uid: String!
  nickname: String!
  contact: UserContact!
  friends: [UserProfile!]!
}
enum<ContactVisibility>()
type<ContactField>()

type<UserContact> {
  property<ContactField?>("discord") {
    resolver { contact, ctx: Context ->
      // THIS POINT
      // Kotlin PSEUDOCODE
      val requested: UserProfile = ... // This is the profile of the user that is being requested, resolved in the previous nodes
      val requester: UserProfile = ctx.get<UserProfile>() // This is the profile of the user who made the request

      when (contact.visibility) {
        PUBLIC -> contact
        PRIVATE -> contact.takeIf { requested == requester } // If the user requested his own contact
        FRIENDS -> contact.takeIf { requested == requester || requested.areFriends(requester) } // If the user requested its own contact or a friend's contact
      }
    }
  }
  ...
}

type<UserProfile>() { ... }
query {
  users {
    contact {
      discord { value }
    }
  }
}

Let's say I want to, in the resolver of root (UserProfile) > contact > discord make checks that will need to use the data available in the root node (check if two users are friends to make the data available or null). How could I do that?

Thank you so much, great framework btw

NathanPB commented 3 years ago

Workaround found:

I'm hacking the Context object and taking advantage of its internal LinkedHashMap to put an entry with the value that I want to pass to the next node. Not very proud of it...

https://github.com/NathanPB/WheresMyDuo/blob/161c1af825f443ed97485927c75062d93c9d53dc/controller/src/main/kotlin/dev/nathanpb/wmd/server/graphql/users.kt#L103L114

https://github.com/NathanPB/WheresMyDuo/blob/161c1af825f443ed97485927c75062d93c9d53dc/controller/src/main/kotlin/dev/nathanpb/wmd/server/graphql/users.kt#L71L97

jeggy commented 3 years ago

I would say you should try to avoid structure like this. And have each resolver to be able to work on it's own. Example below would be how I would do it in a non document database:

data class UserProfile(val id: Int, val nickname: String)
data class UserContact(val userProfileId: Int, val discord: ContactField?, val skype: ContactField?, val ...)
data class ContactField(val value: String, val visibility: ContactVisibility)
...

query("getProfile") {
  resolver { -> UserProfile(1, "jeggy") }
}
type<UserProfile> {
  property<UserContact>("contact") {
    resolver { userProfile -> service.loadContactById(userProfile.id) }
  }
}
type<UserContact> {
  property<ContactField?>("discord") {
    resolver { contact ->
      val requester = ctx.get<UserProfile>()
      contact.discord?.takeIf {
        it.visibility.canView(service.loadUserProfileById(contact.userProfileId), requester?.uid)
      }
    }
  }
}

But ofcourse you are using mongo and kmongo (which I have very little experience with). It will probably be hard to change your data structure to have all the information needed without the need of knowing who the grandparent resolver returned.

jeggy commented 3 years ago

But you how given scenarios for two new features that could be nice to have in KGraphQL, which are: