Best Practices for custom GraphQL resolvers

As I developed a response to this question, I realized it might be something good to share as it’s own thing.

I think it would be cool to get feedback from others on my notes below, and also see what else others have to add.

Cheers!

Use a script to version control everything

You can uploaded a GraphQL schema and bootstrap Indexes, Functions, and Roles at the same time.

Here is an example script to upload schema and run other queries.

Tip for uploading a schema from code: Do NOT use multipart/form-data!

I’ve also published a package that can be used to break a schema into multiple files and use type extensions.

I have not settled on the best way to do this in a modular way, or best way for error handling across all bootstrapping queries. Though I really like keeping the UDFs that are called directly as GraphQL resolvers in a separate file.

Other good practices for this should be considered, but of course are not unique to GraphQL implementation:

Create Wrapper UDFs for GraphQL resolvers.

While using Fauna’s native GraphQL with custom resolvers, I’ve discovered the following:

  • There are a lot of UDFs that are not GraphQL resolvers. I want to keep the separated in code and dashboard.
  • GraphQL UDF signatures (input and returns) are often different than what is good for reusable UDFs.
  • It is helpful to compose UDFs that return a Set, so that you can Union or Intersection them.
  • Some resolvers trigger a whole bunch of logic run in UDFs. I want good reusable UDFs.

This is why I recommend creating a UDF that makes sense in FQL land, then making a separate one for the GraphQL resolver.

I’ve not settled on a naming convention, but I am liking adding gql_ to the beginning for now.

Different inputs

GraphQL wants you to give it IDs. FQL wants the whole Ref.

Resolver UDF creates the Ref given an ID and calls the other UDF.

{
  name: "somethingWithUser",
  body: Query(
    Lambda("userRef", /*...*/)
  )
}
{
  name: "gql_somethingWithUser",
  body: Query(
    Lambda("userId", Call(
      Function("somethingWithUser"), 
      Ref(Collection("User"), Var("userId"))
    ))
  )
}

Different outputs

GraphQL wants the results to be Documents, not Refs. But to make UDFs reusable often means you just want to work with Refs, or you want to return an index with multiple values.

Resolver UDF Maps results to Get the documents.

{
  name: "gql_getSomeThings",
  body: Query(
    Lambda([], Let(
      {  things: Call(Function("getSomeThings")) },
      Lambda("ref", Get(Var("ref")))
    ))
  )
}

More of a How-To, than a best practice, but since Pagination with GraphQL resolvers is a bit confusing, here are some things on that:

Using Pagination Arguments in a UDF

Docs can be helpful, even if they get a bad reputation! They have a good example of how to use pagination arguments in a UDF.

I created a helper function that builds on that example:

const paginateWithOptions = (set, size, after, before) =>
  q.If(
    q.Equals(before, null),
    q.If(
      q.Equals(after, null),
      q.Paginate(set, { size: size }),
      q.Paginate(set, { size: size, after: after })
    ),
    q.Paginate(set, { size: size, before: before })
  )

Use like this:

{
  name: "paginatedResolver",
  body: q.Query(
    q.Lambda(
      ['input', 'size', 'after', 'before'],
      q.Map(
        paginateWithOptions(
          q.Call(q.Function('getSomeSet'), q.Var('input')),
          q.Var('size'),
          q.Var('after'),
          q.Var('before')
        ),
        q.Lambda('ref', q.Get(q.Var('ref')))
      )
    )
  )
})

Where getSomeSet UDF returns a Set, for example from Match(Index(...))

Other Forum references

4 Likes