GraphQL - How to distinguish Public from Private f

2019-04-13 00:30发布

问题:

Context
I have a GraphQL API and a NodeJS & Angular application with a MongoDB database that holds users. For each user, there is a public page with public information like id and username. When a user is logged in, there is a private profile page with extended information like an email.

Just for context, I'm using jsonwebtoken with accesscontrol to authenticate and authorize a user. The information is stored on the Context of every GraphQL resolve function, so whatever is needed to identify a logged in user is available.

I have a GraphQL query that retrieves a public user like so:

query getUserById($id: ID!) {
  getUserById(id: $id) {
    id,
    username
  }
}

I am trying to think of the proper implementation to retrieve either a public or a private user. Since GraphQL is strong typed, I'm having some trouble coming up with a proper solution.

Question
How do I implement the distinction between a public and a private user?

Considerations

1. Separate query
So one of the options is to have a seperate query for both public and private fields:

public query

query getUserById($id: ID!) {
  getUserById(id: $id) {
    id,
    username
  }
}

private query

query getMe {
  getMe {
    id,
    username,
    email
  }
}

2. Using GraphQL Interfaces
I came across this Medium article that explains how GraphQL Interfaces are used to return different Types based on a resolveType function. So I would go something like so:

query getUser($id: ID!) {
   getUser(id: $id) {
     ... on UserPrivate {
       id,
       username
     }
     ... on UserPublic {
       id,
       username,
       email
     }
   }
}

I have not came across a proper solution and I'm unsure about either of the consideration I have so far.

Any help is much appreciated!

回答1:

I think what you are missing here is that in GraphQL you usually want to create this deeply connected graph structure. While getUserByIdand getMe work well as entry points (and I think they are still a great idea even with the interface type), you will most likely have user types coming up all over you schema. Imagine the popular blog post example:

type Post {
  id: ID!
  title: String!
  content: String!
  author: User!
}

Adding two author fields here does not really work very well. Similarly in your example you might not know that the profile page is your own until you get a response from the backend (think about twitter profiles).

Instead, in my opinion there are two methods to consider:

First one is the interface idea. You would have an interface that has all the common fields and concrete implementations for the private and public type. The nice thing here: If you only use the common fields you don't even have to use the type matching:

query getUser($id: ID!) {
   getUser(id: $id) {
     id
     username

     # if you need a private field you can branch off here
     ... on UserPrivate {
       email
     }
   }
}

When it gets more finely grained (people share what they want to expose to the public, imagine Facebook) or you have a lot of types (UserMe, UserFriend, UserStranger) you might want to consider nullable fields instead. If you don't have access to the field you will receive null from the API. To reduce the amount of null checking you can easily bundle fields into their own types (e.g. Address).

Summary:

From the API point it is a bit easier to return nullable fields because it gives you a lot of flexibility. It is much easier to evolve the second option without breaking changes than the first one. Using interfaces is more expressive and surely more fun to work with in the frontend if you work with static types (Typescript, Flow, Scala.js, Reason, etc.). Keyword: Pattern matching.