Creating custom resolvers for Dgraph GraphQL

Checking out the GraphQL tutorial here… https://graphql.dgraph.io but there doesn’t seem to be any examples of how I would add custom resolvers. Any idea when we’ll get some examples for custom resolvers? Thanks!

1 Like

I can be wrong, but I believe we have no support for this.

One way to get around this is to create a graphql instance with your custom resolvers and mix it using Apollo Federation.

Cheers.

1 Like

Thanks @MichelDiz, yes that’s right at the moment. We are looking at providing nice support here, but there’s nothing concrete yet.

is this still the case?

I am sure I will be corrected if I am wrong, but from what I understand:

Version 20.07.0 (which is still in beta) is suppose to have a solid support for the @custom directive. The use case examples tend to not have resolvers per se, but more along the lines of remote endpoints. You could write a lambda script and then use a @custom directive to point to the lambda script to get back the data that you want given the variables provided.

I asked recently about a way to do it without a external script: Custom Field concatenation without needing external script and was referred back to the docs and github examples in source.

What would be nice is a way to use some simple functions inline like math or concatenation functions in the schema to make simple custom resolvers.

I am working out my use case for how to call back to the Dgraph instance to enable one field to be a container of multiple field parts. For instance name = {firstName, middleName, lastName} with a schema like:

type Person {
  id: ID!
  firstName: String
  middleName: String
  lastName: String
  name: String @custom(...)
}

Hi,

20.07.0 will come with support to define custom resolvers as call outs to http endpoints. That’ll mean you can do things like:

  • custom fields/queries/mutations that represent some in-code computation
  • customs that integrate another REST endpoint or another GraphQL endpoint
  • customs that call back to the same Dgraph instance

For the 20.11.0 release we are thinking of allowing custom code that runs in Dgraph, so you can implement some things locally without calling out to an external service. That’ll really match the usecase you mention above.

Our first thought is that we would support javascript (because it’s the most widely used language in the GraphQL ecosystem). Would that work with your thoughts?

7 Likes

I support javascript idea!!

4 Likes

5 Likes

Hi, I’m working on a demo to show to my coworkers the power of Dgraph, I was just trying to setup a custom mutation and got an error. I’m using version 20.03.1 and just want to confirm that this is still not supported til 20.07.0 is released, right?
The error is:

couldn’t rewrite mutation updateGQLSchema because input:17: You don’t need to define the GraphQL Query or Mutation types.

If I want to get this feature, can I pull a specific image like dgraph/standalone:master instead of dgraph/standalone:latest one, right?

Hi @danielsan22
Yes, that’s correct you should try with image of master.

1 Like

I am trying to solve some query limitations in graphql by using a custom resolver; the data is all in dgraph, but the query is too complicated to express in graphql.

Is there a way to create a custom resolver that just returns the uids of a bunch of objects that I want to be expanded on the dgraph/graphql side? That would let me to avoid sending all the payload from dgraph to my resolver service and back again…

Here is kind of what I have so far:

type Item {
  primaryKey: String! @id
  lots: String!
  of: String!
  fields: String!
  here: String!
}

type Query {
  myCrazyQuery(foo: String!): [Item] @custom(http:{
    url: "http://192.168.1.1:9990/graph-callback/myCrazyQuery/$foo",
    method: "GET"
  })
}

but all the payload is going over the wire an extra two times (dgraph → resolver, resolver → dgraph)

No, currently @custom only supports calling out to some HTTP endpoints. It doesn’t yet support full custom logic in which you can write a graphql± query directly. That will be part of 20.11 as @michaelcompton pointed out in the below quote.

But, at present, I think you can cut a hop by directly specifying the /query endpoint of dgraph in the custom URL. That would mean dgraph → dgraph, no resolver in between. See this and the following discussion for more info about how to do that.

1 Like

Thanks!! I had seen the comment about custom code; that would certainly solve my specific use case where the data is stored in the same dgraph, but it might also be that I have some other way of coming up with the ids (like some other database I use for caching or indexing where I can just store the object ids with no need to store the actual data)

(PS and FWIW, rather than having to write in JavaScript, I would look forward to a way to use Go plugins to deploy custom code. In fact, the JavaScript plugin framework could just be a Go plugin. That would let Go developers avoid the extra layer(s) of abstraction and encourage an ecosystem of high performance plugins)

1 Like

Now I think I didn’t understand it fully :thinking:.

What do you want to do with those uids?

Can you explain what do you mean by “expanded”? (I guess this is same as the above question)

What does your resolver service do? (Again, I think this would be same as the above questions I asked :sweat_smile:)

We will consider your suggestion while making the final decision. Thanks for it.

This is a bit of a synthetic example, but it captures the key features of what I’m trying to do…

For example, consider if I am modeling an issue tracking system…

type Issue {
  id: Int! @id
  summary: String!
  type: IssueType!
  description: String!
  status: Status!
  owner: User!
}

type Status {
  label: String!
  icon: String!
}

type IssueType {
  id: Int! @id
  category: String!
}

type Project {
  id: Int! @id
  issues: [Issue!]!
}

type User {
  name: String!
  avatar: String!
  email: String
}

The client has a particular view that wants to display all the issues in a specific project for which the issue type is of category “bug”. Note that there could be multiple issue types with that category, according to the user’s configuration.

As best I can tell, GraphQL doesn’t really define filtering in any useful sense, and Dgraph’s GraphQL bindings can really express that rich of a question (in fact, last I checked, even GraphQL± can’t handle that directly).

So, I can write my own custom resolver:

type Query {
  bugIssues: [Issue] @custom(http:{
    url: "http://192.168.1.1:9990/bugissues",
    method: "GET"
  })
}

Without knowing a priori what substructure of Issue the client is going to query on, that resolver has to return the entire reachable graph from Issues in order to let the client make interesting queries.

query {
  bugIssues {
    id
    summary
  }
}

and since I am pulling the real data from dgraph itself, those objects have to be fully expanded by Dgraph, back to my custom resolver, and then back to Dgraph itself in order for Dgraph’s GraphQL engine to pick out the one or two fields that the graphql client has requested.

What I imagined is that I could just return the uids of the Issue objects, then dgraph could expand those objects (including chasing down any requested relationships) with only the minimal amount of information going over the wire.

Here is an illustration of what I’m talking about:
image
the highlighted data has to be fully hydrated even if the information is not needed to perform the necessary calculations (“crazy join”) just so that Dgraph can get the data needed to satisfy the original client request, even though Dgraph already has the data

1 Like

I am thinking that our advance filtering capabilities will also have to include some extra trips between a filter script and the db endpoint.

Any kind of deeper level queries will be limited with native endpoints. I want to answer questions such as “show contacts that have addresses that have any of these states and the contact has todos that have these qualities and the contact has any of these tags but not these tags and does not have events in the next 6 months.”

In order to do that with a nested graph data I will have to do a query to get the different parts using filters and cascade and then take the ids returned to do some negative filtering level and then return those ids to get the actual fields wanted.

It gets pretty complex when you want to empower the end user through a UI to build nested filters on any top level of data.

I assume you directly want Issue objects in the response, and not Project object, which can contain issues. I feel what you are trying to achieve can be done using GraphQL filters, @hasInverse, and @cascade. No, need to use any custom resolvers. Consider this schema:

type Issue {
  id: String! @id
  summary: String!
  type: IssueType!
  description: String!
  status: Status!
  owner: User!
  project: Project! @hasInverse(field: issues)
}

type Status {
  label: String!
  icon: String!
}

type IssueType {
  id: String! @id
  category: String! @search(by: [hash])
}

type Project {
  id: String! @id
  issues: [Issue!]!
}

type User {
  name: String!
  avatar: String!
  email: String
}

Note that I have just done following things to the previous schema:

  • added project field with @hasInverse inside Issue type
  • added @search on IssueType.category
  • changed @Id field types to String from Int.

Now, if you do this query, it would fetch only those issues in “Project 2” whose type.category is “bug”, irrespective of whether the type.id is the same or not.

query {
  queryIssue @cascade {
    id
    summary
    type (filter: {category: {eq: "bug"}}) {
      __typename
    }
    project(filter: {id: {eq: "Project 2"}}) {
      __typename
    }
  }
}

I don’t think you would need to use any custom resolvers for this case.
Does this solve your problem?

In general, I agree that at present GraphQL can’t do queries using all sorts of logic. It generates a fixed API for you to use, and you can only do the queries in that API. And, that is the reason we are going to support GraphQL± by 20.11 release, so that everything you can do in ± is possible in GraphQL too.

could you please elaborate on what exactly was GraphQL± unable to handle?

3 Likes

Woohoo!! :partying_face:

Hi Anthony, have you success working in a field to be a container of multiple field parts?

Yes! Here is how:

Schema snippet:

type Contact {
  id: ID
  prefix: String
  firstName: String
  nickName: String
  middleName: String
  lastName: String
  suffix: String
  organization: String
  abbreviation: String
  name: String @lambda
}

Then in my lambda script I have:

const contactNameResolver = ({ parent: { prefix, firstName, nickName, middleName, lastName, suffix, organization, abbreviation } }) => {
  let name = ''
  if (organization) {
    name = organization
    if (abbreviation) name += ` (${abbreviation})`
  } else {
    if (prefix) name = prefix
    if (firstName) name += ` ${firstName}`
    if (nickName) name += ` "${nickName}"`
    if (middleName) name += ` ${middleName}`
    if (lastName) name += ` ${lastName}`
    if (suffix) name += ` ${suffix}`
    name = name.trim()
  }
  return name
}

self.addGraphQLResolvers({
  "Contact.name": contactNameResolver
})

The magic part is the self.addGraphQLResolvers function. Map the key to the [type].[field] and then for the value give it the name of the function. In the contactNameResolver function I deconstruct the parent object to get the name parts and then do some basic js logic to build the desired string. Then just return the string.

This is probably the simplest use case for a lambda function minus the string building logic. For a really simple use case, the contactNameResolver could just be:

const contactNameResolver = ({ parent: { firstName, lastName } }) => `${firstName} ${lastName}`.trim()

The trim here is to remove any white space if the contact is missing either the first name or the last name fields.

3 Likes