How to Write Efficient Queries for GraphQL Resolvers
Ryan Chenkie
@ryanchenkie
Discoverability
Client/Server Contracts
Efficiencies
Discoverability
Client/Server Contracts
Efficiencies
(Abstract Syntax Tree)
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
}
}
}
Query: {
books: (parent, args, context, info) => {}
}
In the resolver function, the info object has the GraphQL 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
}
}
}
{
"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"
}
]
}
}
[
{
"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",
}
]
query {
books {
title
author
publishDate
}
}
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'
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);
}
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)
);
}
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
@AdamRackis
Ryan Chenkie
@ryanchenkie