How to return a DQL query result from a custom lambda resolver

Assuming running a DQL mutation inside a custom lambda resolver

{
  q(func: uid(0x1)) @recurse(loop: false, depth: 3) {
    uid
    dgraph.type
    expand(_all_)
  }
}

and the response of which will be

{
  "data": {
     "q": [
       0: {
         "uid": "0x1",
         "dgraph.type": ["Animal", "Cat"],
         "Cat.name": "Garfield"
       }
     ]
  }
}

What I do

I then return this value from my lambda resolver back to the fronend which obviously only understands GraphQL and thus I need to convert the DQL result.

I currently simply run a function which strips the typename from the key

export const dqlToGraphQl: DqlToGraphQl = params => {
  let newObject = params.assignNew && {};
  Object.entries(params.object).forEach(([key, value]) => {
    let newKey = key;
    let newValue = value;

    if (newKey === "uid") {
      newKey = "id";
    } else {
      newKey = key.split(".")[1];
    }

    // Value is a regular object
    if (typeof newValue === "object" && !Array.isArray(newValue) && newValue !== null) {
      dqlToGraphQl({ object: newValue, types: params.types });
    }

    // Value is an Array
    if (Array.isArray(newValue)) {
      newValue.forEach(val => {
        if (typeof val === "object" && !Array.isArray(val) && val !== null) {
          dqlToGraphQl({ object: val, types: params.types });
        }
      });
    }
    delete Object.assign(newObject || params.object, { [newKey]: newValue })[key];
  });

  return newObject;
};

This works fine and somehow (even if I don’t actively return __typename), typenames get returned properly unless I have a union type or an interface and thus the dgraph.type array of a length > 1.

So how does Dgraph does this internally? How would I parse the DQL result to attach the proper typenames?

Thanks!

1 Like

Alright, not sure if this is the right approach but since nobody has or wants to respond to those (my) questions in this forum, I post my solution in case someone else runs into the same problem.

DISCLAIMER - I’m not sure if this is the right way of doing it, but at least it works for me.

Summary of the task

When performing a DQL query inside eg. a custom lambda resolver which then should be returned back to a GraphQL frontend, the DQL result needs to be parsed. According to the GraphQL specs, not only the mandatory fields have to be returned but also __typename which uniquely identifies each type in the GraphQL schema.

Since Dgraph treats Interfaces and Unions as types, a DQL query for a type can return an array of Dgraph Types (in fact dgraph.type, which is always an array). In order to return the appropriate typename we therefore have to “extract” the right typename from the array.

Solution

Step 1

Before bundling my lambda with webpack I parse the schema file (in my case schema.graphql) and extract all interface names. I then create a file which exports an array of those interface names.

export const parseInterfacesFromSchema = () => {
  const schema = readFileSync(`${monorepoEnvironment.dgraphSchemaFileLocation}`, "utf-8")
    .split("\n")
    .filter(a => a.includes("interface"));
  const interfaces = schema.map(t => t.split(" ")[1]);

  const file = createWriteStream(`${monorepoEnvironment.clientSchemaLocation}/interfaces.ts`);
  file.on("error", e => {
    throw new Error(e.message);
  });
  file.write("/**\r\n");
  file.write(" * List of interfaces in the current schema. We need this for corrent __typename\r\n");
  file.write(" * parsing when returning DQL queries.\r\n");
  file.write(" *\r\n");
  file.write(" * This file has been autogenerated by 'parseInterfaceFromSchema'.\r\n");
  file.write(" */\r\n");
  file.write("export const interfaces = [\r\n");
  interfaces.forEach(i => file.write(`\t"${i}",\r\n`));
  file.write("];");
  file.end();
};

Step 2

This step happens when resolving the lambda. I parse the DQL result and

  • change uid → id
  • remove all types from the predicate, so Cat.name becomes name
  • rename the key dgraph.type to __typename
  • remove all interface types from the dgraph.type array which leaves me with the correct typename

Here’s the parser:

import { interfaces } from 'path/to/interfaces.ts';

export const dqlToGraphQl: DqlToGraphQl = params => {
  let newObject = params.assignNew && {};
  Object.entries(params.object).forEach(([key, value]) => {
    let newKey = key;
    let newValue = value;

    // Convert `uid` --> `id`
    if (newKey === "uid") {
      newKey = "id";
    }

    // Parse typenames
    else if (newKey === "dgraph.type") {
      newKey = "__typename";
      const types = newValue as string[];

      // Set the type value
      newValue = types[0];

      // Strip interfaces from type if interfaces are implemented
      if (types.length > 1) {
        newValue = types.filter(type => !interfaces.includes(type))[0];
      }
    }

    // Strip typenames from predicates and return field name
    else {
      newKey = key.split(".")[1];
    }

    // Value is a regular object
    if (typeof newValue === "object" && !Array.isArray(newValue) && newValue !== null) {
      dqlToGraphQl({ object: newValue, types: params.types });
    }

    // Value is an Array
    if (Array.isArray(newValue)) {
      newValue.forEach(val => {
        if (typeof val === "object" && !Array.isArray(val) && val !== null) {
          dqlToGraphQl({ object: val, types: params.types });
        }
      });
    }
    delete Object.assign(newObject || params.object, { [newKey]: newValue })[key];
  });

  return newObject;
};

Hope this helps!

If someone from the Core Team finds some time to read this and is willing to comment on this solution, I’d be more than happy!

I’m pretty sure there’s a better solution…

1 Like