Custom DQL __typename bug / strange behaviour

I tried out a little bit the Custom DQL feature which came out with the 20.07.0 .
It apparently gives a little bit of integration between GraphQL and GraphQL± but I found a quite big limitation:

given the schema

interface Metadata {
  id: String! @id @search(by: [hash])
}

interface WithName {
  names: [TextWrapper!]!
}

type TextWrapper implements Metadata {
  text: Text! 
  localization: String!
  isName: [WithName!]! @hasInverse(field: names)
}

type Text implements Metadata {
  string: String! @search(by: [trigram, term, fulltext])
}

type Person implements Metadata & WithName {
    age: Int!
    parents: [Person!]!
}

type Company implements Metadata & WithName {
    address: String!
}

extend type Query {
  queryByName(search: String!): [WithName] @custom(dql: """
    query q($search: string) {
        var(func: anyoftext(Text.string, $search)) {
            ~TextWrapper.text {
              ~WithName.names {
                u as uid
              }
            }
        }
        queryByName(func: uid(u)) {
            id: Metadata.id
            names : WithName.names
			age: Person.age
			parents: Person.parents
			address: Company.address
        }
    }
    """)
}

, updated the relation needed from queryByName to traverse reverse edges:

<TextWrapper.text>: uid @reverse .
<WithName.names>: [uid] @reverse .

and populaed the db with some data

mutation {
  addText(input:[
  {id: "cccc", string: "Mario"},
     {id: "eeee", string: "マリオ"},
    {id: "hhhh", string: "Leonardo"},
    {id: "jjjjj", string: "レオナルド"},
    {id: "qwer", string: "Shueisha"},
  ]) {
    numUids
  }
  addPerson(input:[{
    id: "aaaa",
    age: 23,
    names: [
      {
      id: "bbbb",
      text: {id: "cccc"},
      localization: "it",
    },
    {
      id: "dddd",
      text: {id: "eeee"},
      localization: "ja",
    }
    ],
    parents:[{
    id: "ffff",
    age: 50,
    names: [{
      id: "gggg",
      text: {id: "hhhh"},
      localization: "it",
    },
    {
      id: "iiiii",
      text: {id: "jjjjj"},
      localization: "ja",
    }],
      parents:[],
    }]
    }]
  ) {
    numUids
  }
  addCompany(input:[{
    names: [{
      id: "dfsdf",
      text: {id: "qwer"},
      localization: "it"
    }],
    id: "shu",
    address: "via Roma"
  }]) {
    numUids
  }
}

I tried to use the DQL query:

  queryByName(search:"Mario") {
    __typename
    ... on Person {
      id
      age
      parents {
        id
      }
    }
  }

result:

{
  "data": {
    "queryByName": [
      {
        "__typename": "WithName",
        "id": "aaaa",
        "age": 23,
        "parents": []
      }
    ]
  },
  "extensions": {
    "touched_uids": 8,
    "tracing": {
      "version": 1,
      "startTime": "2020-08-10T22:17:08.93566995Z",
      "endTime": "2020-08-10T22:17:08.940677751Z",
      "duration": 5007701,
      "execution": {
        "resolvers": [
          {
            "path": [
              "queryByName"
            ],
            "parentType": "Query",
            "fieldName": "queryByName",
            "returnType": "[WithName]",
            "startOffset": 91300,
            "duration": 4900601,
            "dgraph": [
              {
                "label": "query",
                "startOffset": 95200,
                "duration": 4867001
              }
            ]
          }
        ]
      }
    }
  }
}

As specified in the Documentation, it’s possible to query only data already retrieved from the queryByName itself and therefore parents is empty.

What I want to point out is that the __typename returned is WithName, which makes sense to some degree.

The real problem is when I tried to put an Apollo server in front of my dgraph. Using this repo I generated everything needed to call the gateway’s graphql and delegate to the dgraph GraphQL the execution of the queries.
In order to resolve the interface type i also added this custom resolver to the Apollo Server

WithName: {
      __resolveType: (obj, context, info) => {
        console.log(obj)
        if (obj.age) {
          return 'Person'
        }
        if (obj.address) {
          return 'Company'
        }
        return obj.__typename
      },
    },

in order to get the underlying type of the interface returned

  queryByName(search:"Mario") {
    __typename
    ... on Person {
      id
      age
      parents {
        id
      }
    }
  }

calling this query, which is the same, I got a super bad error

{
  "errors": [
    {
      "message": "Abstract type SearchResult must resolve to an Object type at runtime for field Query.queryByName with value {}, received \"undefined\". Either the SearchResult type should provide a \"resolveType\" function or each possible type should provide an \"isTypeOf\" function.",
      "locations": [
        {
          "line": 16,
          "column": 3
        }
      ],
      "path": [
        "queryByName",
        0
      ],
      "extensions": {
        "code": "INTERNAL_SERVER_ERROR",
        "exception": {
          "message": "Abstract type SearchResult must resolve to an Object type at runtime for field Query.queryByName with value {}, received \"undefined\". Either the SearchResult type should provide a \"resolveType\" function or each possible type should provide an \"isTypeOf\" function.",
          "locations": [
            {
              "line": 16,
              "column": 3
            }
          ],
          "stacktrace": [
            "GraphQLError: Abstract type SearchResult must resolve to an Object type at runtime for field Query.queryByName with value {}, received \"undefined\". Either the SearchResult type should provide a \"resolveType\" function or each possible type should provide an \"isTypeOf\" function.",
            "    at ensureValidRuntimeType (A:\\Windows\\Users\\Luscha\\Desktop\\GIT\\Personal\\esperiments\\graphql-gateway-test\\node_modules\\graphql\\execution\\execute.js:667:11)",
            "    at completeAbstractValue (A:\\Windows\\Users\\Luscha\\Desktop\\GIT\\Personal\\esperiments\\graphql-gateway-test\\node_modules\\graphql\\execution\\execute.js:660:42)",
            "    at completeValue (A:\\Windows\\Users\\Luscha\\Desktop\\GIT\\Personal\\esperiments\\graphql-gateway-test\\node_modules\\graphql\\execution\\execute.js:585:12)",
            "    at completeValueCatchingError (A:\\Windows\\Users\\Luscha\\Desktop\\GIT\\Personal\\esperiments\\graphql-gateway-test\\node_modules\\graphql\\execution\\execute.js:495:19)",
            "    at A:\\Windows\\Users\\Luscha\\Desktop\\GIT\\Personal\\esperiments\\graphql-gateway-test\\node_modules\\graphql\\execution\\execute.js:618:25",
            "    at Array.forEach (<anonymous>)",
            "    at forEach (A:\\Windows\\Users\\Luscha\\Desktop\\GIT\\Personal\\esperiments\\graphql-gateway-test\\node_modules\\iterall\\index.js:83:25)",
            "    at completeListValue (A:\\Windows\\Users\\Luscha\\Desktop\\GIT\\Personal\\esperiments\\graphql-gateway-test\\node_modules\\graphql\\execution\\execute.js:614:24)",
            "    at completeValue (A:\\Windows\\Users\\Luscha\\Desktop\\GIT\\Personal\\esperiments\\graphql-gateway-test\\node_modules\\graphql\\execution\\execute.js:573:12)",
            "    at A:\\Windows\\Users\\Luscha\\Desktop\\GIT\\Personal\\esperiments\\graphql-gateway-test\\node_modules\\graphql\\execution\\execute.js:492:16"
          ]
        }
      }
    }
  ],
  "data": {
    "queryByName": [
      null
    ]
  }
}

which do not make any sense, given the fact I specified a resolver for the __typename

Digging further i put some logs int he query resolver:

export function buildQuery(schema: GraphQLSchema): QueryResolvers {
  return {
    queryByName: async (root, args, context, info) => {
      console.log("here")
      console.log(args)
      var a = await delegateToSchema({
        schema,
        operation : "query",
        fieldName: "queryByName",
        args,
        context,
        info
      });
      console.log("here 2")
      
      a.forEach(element => {
        console.log(element)
      });
      return a
    }
....

and the result is

here
{ search: 'mario' }
here 2
 {
   [Symbol(subSchemaErrors)]: [
     {
       message: '__typename did not match an object type: WithName',
       locations: [],
       path: []
     }
   ]
 }

which makes totally sense. Dgraph returns an object with __typename WithName, as reported some queries ago, but apollo expects a result which implements WithName, in our case Person or Company.

I could not figure out how should behave dgraph in those case, either not allowing the return of an inteface in a @custom query, or giving the possibility to resolve / harcode a default __typename internally.
Anyway at the moment the feature do not behave with compliance to Apollo and i guess the GrapghQL spec in general.

You can just add dgraph.type edge in the dql query, and __typename would work as you expect it to. So, this should work:

type Query {
  queryByName(search: String!): [WithName] @custom(dql: """
    query q($search: string) {
        var(func: anyoftext(Text.string, $search)) {
            ~TextWrapper.text {
              ~WithName.names {
                u as uid
              }
            }
        }
        queryByName(func: uid(u)) {
            dgraph.type
            id: Metadata.id
            names : WithName.names
			age: Person.age
			parents: Person.parents
			address: Company.address
        }
    }
    """)
}

Note that you don’t need to alias it.

The GraphQL layer uses dgraph.type for interfaces to figure out the type of implementation. And, as it is not returned from custom DQL, it won’t be able to know the correct type. So, you would need to add it.
I will update the docs for this.

Also, note that you don’t need to mention the extend keyword. The default behaviour is to extend the auto-generated CRUD API with the user-provided Query type.

Tried and works!
Thank you

1 Like