What about the Database?

How to Write Efficient Queries for GraphQL Resolvers

Ryan Chenkie

@ryanchenkie

  • Full Stack Developer
  • Google Developer Expert
  • JavaScript Consultant
  • React, Angular, Node, GraphQL

Why Do We ❤️ GraphQL?

Discoverability

Client/Server Contracts

Efficiencies

Why Do We ❤️ GraphQL?

Discoverability

Client/Server Contracts

Efficiencies

When you make a query, GraphQL resolves with exactly the data you ask for

But what does the database do?

We might get efficiencies out of GraphQL itself, but we might be losing out at the database layer

How can we use GraphQL to improve our database queries?

  1. Scoped Selections
  2. Sorts and Limits from Sub-Selections
  3. Scoped Queries from Intersecting Auth Info
  4. Protection from Deeply Nested Queries

Use Cases

MongoDB by example, but applicable to any database

It's all about the AST

(Abstract Syntax Tree)

There's code for humans and then there's code for computers

GraphQL for Humans

query {
  books {
    title
    author
    publishDate
  }
}

GraphQL for

Computers

{
  "fieldName": "books",
  "fieldNodes": [
    {
      "kind": "Field",
      "name": {
        "kind": "Name",
        "value": "books",
        "loc": {
          "start": 4,
          "end": 9
        }
      },
      "arguments": [],
      "directives": [],
      "selectionSet": {
        "kind": "SelectionSet",
        "selections": [
          {
            "kind": "Field",
            "name": {
              "kind": "Name",
              "value": "title",
              "loc": {
                "start": 16,
                "end": 21
              }
            },
            "arguments": [],
            "directives": [],
            "loc": {
              "start": 16,
              "end": 21
            }
          },
          {
            "kind": "Field",
            "name": {
              "kind": "Name",
              "value": "author",
              "loc": {
                "start": 26,
                "end": 32
              }
            },
            "arguments": [],
            "directives": [],
            "loc": {
              "start": 26,
              "end": 32
            }
          },
          {
            "kind": "Field",
            "name": {
              "kind": "Name",
              "value": "publishDate",
              "loc": {
                "start": 37,
                "end": 48
              }
            },
            "arguments": [],
            "directives": [],
            "loc": {
              "start": 37,
              "end": 48
            }
          }
        ],
        "loc": {
          "start": 10,
          "end": 52
        }
      },
      "loc": {
        "start": 4,
        "end": 52
      }
    }
  ],
  "returnType": "[Book]",
  "parentType": "Query",
  "path": {
    "key": "books"
  },
  "schema": {
    "__validationErrors": [],
    "__allowedLegacyNames": [],
    "_queryType": "Query",
    "_directives": [
      "@cacheControl",
      "@skip",
      "@include",
      "@deprecated"
    ],
    "_typeMap": {
      "Query": "Query",
      "Book": "Book",
      "String": "String",
      "Int": "Int",
      "Contact": "Contact",
      "Address": "Address",
      "Secret": "Secret",
      "Post": "Post",
      "Author": "Author",
      "__Schema": "__Schema",
      "__Type": "__Type",
      "__TypeKind": "__TypeKind",
      "Boolean": "Boolean",
      "__Field": "__Field",
      "__InputValue": "__InputValue",
      "__EnumValue": "__EnumValue",
      "__Directive": "__Directive",
      "__DirectiveLocation": "__DirectiveLocation",
      "CacheControlScope": "CacheControlScope",
      "Upload": "Upload"
    },
    "_possibleTypeMap": {},
    "_implementations": {},
    "_extensionsEnabled": true
  },
  "fragments": {},
  "operation": {
    "kind": "OperationDefinition",
    "operation": "query",
    "variableDefinitions": [],
    "directives": [],
    "selectionSet": {
      "kind": "SelectionSet",
      "selections": [
        {
          "kind": "Field",
          "name": {
            "kind": "Name",
            "value": "books",
            "loc": {
              "start": 4,
              "end": 9
            }
          },
          "arguments": [],
          "directives": [],
          "selectionSet": {
            "kind": "SelectionSet",
            "selections": [
              {
                "kind": "Field",
                "name": {
                  "kind": "Name",
                  "value": "title",
                  "loc": {
                    "start": 16,
                    "end": 21
                  }
                },
                "arguments": [],
                "directives": [],
                "loc": {
                  "start": 16,
                  "end": 21
                }
              },
              {
                "kind": "Field",
                "name": {
                  "kind": "Name",
                  "value": "author",
                  "loc": {
                    "start": 26,
                    "end": 32
                  }
                },
                "arguments": [],
                "directives": [],
                "loc": {
                  "start": 26,
                  "end": 32
                }
              },
              {
                "kind": "Field",
                "name": {
                  "kind": "Name",
                  "value": "publishDate",
                  "loc": {
                    "start": 37,
                    "end": 48
                  }
                },
                "arguments": [],
                "directives": [],
                "loc": {
                  "start": 37,
                  "end": 48
                }
              }
            ],
            "loc": {
              "start": 10,
              "end": 52
            }
          },
          "loc": {
            "start": 4,
            "end": 52
          }
        }
      ],
      "loc": {
        "start": 0,
        "end": 54
      }
    },
    "loc": {
      "start": 0,
      "end": 54
    }
  },
  "variableValues": {},
  "cacheControl": {
    "cacheHint": {
      "maxAge": 0
    }
  }
}

A GraphQL query is translated into something the computer can understand

That something takes the form of an Abstract Syntax Tree (AST)

Abstract

Can be anything

Syntax

Has to do with programming syntax

Tree

One big object with many nested objects

ASTs are useful because we can extend GraphQL's capabilities programmatically

How can we interact with the AST?

Query: {
  books: (parent, args, context, info) => {}
}

In the resolver function, the info object has the GraphQL AST

Useful pieces of the AST

fieldNode
selectionSet
selections
query {
  books {
    title
    author
    publishDate
  }
}
{
  "fieldName": "books",
  "fieldNodes": [
    {
      "kind": "Field",
      "name": {
        "kind": "Name",
        "value": "books",
        "loc": {
          "start": 4,
          "end": 9
        }
      },
      "arguments": [],
      "directives": [],
      "selectionSet": {
        "kind": "SelectionSet",
        "selections": [
          {
            "kind": "Field",
            "name": {
              "kind": "Name",
              "value": "title",
              "loc": {
                "start": 16,
                "end": 21
              }
            },
            "arguments": [],
            "directives": [],
            "loc": {
              "start": 16,
              "end": 21
            }
          },
          {
            "kind": "Field",
            "name": {
              "kind": "Name",
              "value": "author",
              "loc": {
                "start": 26,
                "end": 32
              }
            },
            "arguments": [],
            "directives": [],
            "loc": {
              "start": 26,
              "end": 32
            }
          },
          {
            "kind": "Field",
            "name": {
              "kind": "Name",
              "value": "publishDate",
              "loc": {
                "start": 37,
                "end": 48
              }
            },
            "arguments": [],
            "directives": [],
            "loc": {
              "start": 37,
              "end": 48
            }
          }
        ],
        "loc": {
          "start": 10,
          "end": 52
        }
      },
      "loc": {
        "start": 4,
        "end": 52
      }
    }
  ],
  "returnType": "[Book]",
  "parentType": "Query",
  "path": {
    "key": "books"
  },
  "schema": {
    "__validationErrors": [],
    "__allowedLegacyNames": [],
    "_queryType": "Query",
    "_directives": [
      "@cacheControl",
      "@skip",
      "@include",
      "@deprecated"
    ],
    "_typeMap": {
      "Query": "Query",
      "Book": "Book",
      "String": "String",
      "Int": "Int",
      "Contact": "Contact",
      "Address": "Address",
      "Secret": "Secret",
      "Post": "Post",
      "Author": "Author",
      "__Schema": "__Schema",
      "__Type": "__Type",
      "__TypeKind": "__TypeKind",
      "Boolean": "Boolean",
      "__Field": "__Field",
      "__InputValue": "__InputValue",
      "__EnumValue": "__EnumValue",
      "__Directive": "__Directive",
      "__DirectiveLocation": "__DirectiveLocation",
      "CacheControlScope": "CacheControlScope",
      "Upload": "Upload"
    },
    "_possibleTypeMap": {},
    "_implementations": {},
    "_extensionsEnabled": true
  },
  "fragments": {},
  "operation": {
    "kind": "OperationDefinition",
    "operation": "query",
    "variableDefinitions": [],
    "directives": [],
    "selectionSet": {
      "kind": "SelectionSet",
      "selections": [
        {
          "kind": "Field",
          "name": {
            "kind": "Name",
            "value": "books",
            "loc": {
              "start": 4,
              "end": 9
            }
          },
          "arguments": [],
          "directives": [],
          "selectionSet": {
            "kind": "SelectionSet",
            "selections": [
              {
                "kind": "Field",
                "name": {
                  "kind": "Name",
                  "value": "title",
                  "loc": {
                    "start": 16,
                    "end": 21
                  }
                },
                "arguments": [],
                "directives": [],
                "loc": {
                  "start": 16,
                  "end": 21
                }
              },
              {
                "kind": "Field",
                "name": {
                  "kind": "Name",
                  "value": "author",
                  "loc": {
                    "start": 26,
                    "end": 32
                  }
                },
                "arguments": [],
                "directives": [],
                "loc": {
                  "start": 26,
                  "end": 32
                }
              },
              {
                "kind": "Field",
                "name": {
                  "kind": "Name",
                  "value": "publishDate",
                  "loc": {
                    "start": 37,
                    "end": 48
                  }
                },
                "arguments": [],
                "directives": [],
                "loc": {
                  "start": 37,
                  "end": 48
                }
              }
            ],
            "loc": {
              "start": 10,
              "end": 52
            }
          },
          "loc": {
            "start": 4,
            "end": 52
          }
        }
      ],
      "loc": {
        "start": 0,
        "end": 54
      }
    },
    "loc": {
      "start": 0,
      "end": 54
    }
  },
  "variableValues": {},
  "cacheControl": {
    "cacheHint": {
      "maxAge": 0
    }
  }
}

Useful Resources on ASTs

bit.ly/gql-ast-1

bit.ly/gql-ast-2

1. Scoped Selections

What GraphQL Gives Back

{
  "data": {
    "books": [
      {
        "title": "Harry Potter and the Chamber of Secrets",
        "author": "J.K. Rowling",
        "publishDate": "1998-07-02"
      },
      {
        "title": "Jurassic Park",
        "author": "Michael Crichton",
        "publishDate": "1990-11-20"
      }
    ]
  }
}

What the DB Gives Back

[
  {
    "title": "Harry Potter and the Chamber of Secrets",
    "author": "J.K. Rowling",
    "publishDate": "1998-07-02",
    "coverArt": "Cliff Wright",
    "country": "United Kingdom",
    "mediaType": "Print",
    "pages": "251",
    "isbn": "0-7475-3849-2",
  },
  {
    "title": "Jurassic Park",
    "author": "Michael Crichton",
    "publishDate": "1990-11-20",
    "coverArt": "Chip Kidd",
    "country": "United States",
    "mediaType": "Print",
    "pages": "400",
    "isbn": "0-394-58816-9",
  }
]

Let's use the selections in the GraphQL query to inform the database query

We need to turn this

query {
  books {
    title
    author
    publishDate
  }
}

Into this

Book.find().select('title author publishDate')
const getQuerySelections = ({ fieldNodes }) => {
  return fieldNodes
    .map(node => node.selectionSet.selections)
    .flat()
    .map(s => s.name.value)
    .join(' ');
};
Query: {
  books: async (parent, args, context, info) => {
    try {
      const selections = getQuerySelections(info);
      return await getBooks(selections);
    } catch (err) {
      throw new Error(err);
    }
  }
}
const selections = getQuerySelections(info);

console.log(selections); // 'title author publishDate'

2. Sorts and Limits from Sub-Selections

We can get arguments from the GraphQL resolver, but what about arguments for sub-selections?

Query: {
  contacts: (parent, args, context, info) => {
    console.log(args);
    // { LIMIT: 10 }  
  }
}
{
  contacts(LIMIT: 10) {
    firstName
    lastName
  }
}
{
  usersWithContacts {
    firstName
    lastName
    contacts(LIMIT: 4) {
      firstName
      lastName
    }
  }
}
Query: {
  booksWithContacts: (parent, args, context, info) => {
    console.log(args);
    // {}
  }
}
const getQuerySubArguments = ({ fieldNodes }) => {
  return fieldNodes
    .map(node => node.selectionSet.selections)
    .flat()
    .filter(s => s.arguments && s.arguments.length)
    .map(s => s.arguments)
    .flat()
    .filter(a => a.kind === 'Argument');
};
const getLimit = rawArgs => {
  const limitArg = rawArgs.find(a => a.name.value === 'LIMIT');
  return limitArg && limitArg.value ? parseInt(limitArg.value.value) : null;
};
usersWithContacts: async (parent, args, context, info) => {
  const subArguments = getQuerySubArguments(info);
  const limit = getLimit(subArguments);
  return await getUsersWithContacts(selections, limit);
}

3. Intersecting Auth

We don't always need to use the AST

const server = new ApolloServer({
  typeDefs,
  resolvers,
  context: ({ req }) => ({
    clientId: getClientId(req.headers.authorization)
  })
});
secrets: async (parent, args, context, info) => {
  const { STARTS_WITH } = args;
  const { clientId } = context;
  
  const rootQuery = { 
    message: { 
      $regex: STARTS_WITH 
    } 
  }
  
  return await getSecrets(
    makeScopedQuery(rootQuery, clientId)
  );
}

4. Defending Against Deeply-Nested Queries

You might have circular relationships in your data

post => author => post => author => post => author
{
  posts {
    author {
      posts {
        author {
          posts {
            author {
              posts {
                author {
                  posts {
                    name
                  }
                }
              }
            }
          }
        }
      }
    }
  }
}
const getSelectionDepth = (node, currentDepth = 1) => {
  return node.map(n => {
    if (!n.selectionSet) {
      return currentDepth;
    }
    return Math.max(
      ...getSelectionDepth(
        n.selectionSet.selections, 
        currentDepth + 1
      )
    );
  });
};
posts: async (parent, args, context, info) => {
  const { fieldNodes } = info;
  const selectionDepth = getSelectionDepth(fieldNodes)[0];

  if (selectionDepth > 5) {
    throw new Error('Max selection depth exceeded');
  }
}
npm install graphql-depth-limit

Limitations

Coupling to the database layer

You may not have full control over your database

Who is doing this in practice?

@AdamRackis

Some Treats for You

Slides

bit.ly/graphql-ast-slides

Code

bit.ly/graphql-ast-demo

graphcollective.io

@graphcollective

Thanks!

Ryan Chenkie

@ryanchenkie

Made with Slides.com