Skip to main content

Working With Field Maps

Field Maps are one of the most essential pieces of GraphQL-Utils. They are a parsed version of the GraphQL AST simplified to take fragments into account as well as remove the extra data we aren't interested in.

You can read more about the FieldMap interface here.

In order to retrieve the field map we can use the resolveFieldMap() helper, which takes info as its only required argument, and we may additionally specify whether a deep field map should be parsed, so we can check nested data and calculate the cost of a query, or just a single layer for example to help us optimize our SQL SELECT and JOIN queries.

Resolving A Field Map#

In order to resolve a field map, all we need to do is use the info argument passed to our GraphQL resolvers, and retrieve the field map using the resolveFieldMap() helper, we can pass various options to adjust the output.

Let's take a look at an example query:

{
posts {
id
title
body
author {
id
username
firstName
lastName
}
}
}

Deep#

If we want the deep field map without any additional configuration, all we need to do is call resolveFieldMap() with our info object:

import { resolveFieldMap } from "@jenyus-org/graphql-utils";
const resolvers = {
Query: {
posts(_, __, ___, info) {
const fieldMap = resolveFieldMap(info);
console.log(fieldMap);
},
},
};

This will output the following field map:

{
posts: {
id: {},
title: {},
body: {},
author: {
id: {},
username: {},
firstName: {},
lastName: {},
},
}
}

Now, if we want to know which columns to SELECT from our database, we can use a simple filter to only retrieve those fields without subselections, and then print those keys:

const fields = Object.entries(fieldMap.posts)
.filter(([key, values]) => Object.keys(values).length)
.map(([key, _]) => key);
console.log(fields);

Our output will be as follows:

["id", "title", "body"]

Flat#

We can also resolve a flat field map, if we only want to use it to e.g. optimize SQL queries. The flat field map will be able to tell us which columns we need to SELECT, which can be used in combination with a query builder like KnexJS or ORMs like MikroORM to make more efficient calls to our database.

For this we simply pass false to the second argument of resolveFieldMap():

import { resolveFieldMap } from "@jenyus-org/graphql-utils";
const resolvers = {
Query: {
posts(_, __, ___, info) {
const fieldMap = resolveFieldMap(info, false);
console.log(fieldMap);
},
},
};

This will output the following field map:

{
posts: {
id: {},
title: {},
body: {},
author: {},
}
}

Then, we could use Object.keys() to get a list of all the selected fields:

const fields = Object.keys(fieldMap.posts);
console.log(fields);

Our output will be as follows:

["id", "title", "body", "author"]

Since author isn't a field we can just SELECT from our database, it might make more sense to use the deep field map to filter out fields with subselections as we showed above.

Under A Specified Parent#

If we only want to retrieve the field map under a specified parent, we can use dot-notation or an array of strings to specify this parent. This can be useful for queries that are computed instead of direct entrypoints to database tables, and we need to get all the fields requested for each entity.

Let's change up our query a bit:

{
postsOverview {
topPosts {
id
title
body
author {
id
username
firstName
lastName
}
}
risingPosts {
id
title
body
author {
id
username
firstName
lastName
}
}
}
}

Now we can specify the parent under which we want to get our field map from with either of the two syntaxes:

  • "postsOverview.topPosts"
  • ["postsOverview", "topPosts"]

The rest of how resolveFieldMap() works is identical to the previous examples, we just need to make sure to specify deep in the arguments:

import { resolveFieldMap } from "@jenyus-org/graphql-utils";
const resolvers = {
Query: {
postsOverview(_, __, ___, info) {
const fieldMap = resolveFieldMap(info, true, "postsOverview.topPosts");
console.log(fieldMap);
},
},
};

This will output the following field map:

{
topPosts: {
id: {},
title: {},
body: {},
author: {
id: {},
username: {},
firstName: {},
lastName: {},
},
}
}

Usage with KnexJS#

Using deep field maps, we can check for fields that don't have any subselections, and assume they are database columns. Using KnexJS we can build dynamic SQL SELECT statements this way:

import { resolveFieldMap } from "@jenyus-org/graphql-utils";
const resolvers = {
Query: {
posts(_, { db }, ___, info) {
const fieldMap = resolveFieldMap(info);
const columns = Object.entries(fieldMap.posts)
// check if field doesn't have subselections
.filter(([_, selections]) => !Object.keys(selections).length)
.map(([key]) => key);
return db.select(...columns).from("posts");
},
},
};

Usage with MikroORM#

Similar to how we did dynamic field selections with KnexJS, we can use MikroORM's entity repository fields and populate options to not only dynamically SELECT specific columns, but also pre-populated relational fields using SQL JOINs if we assume fields with subselections are relations in our MikroORM entities:

import { resolveFieldMap } from "@jenyus-org/graphql-utils";
const resolvers = {
Query: {
posts(_, { postsRepository }, ___, info) {
const fieldMap = resolveFieldMap(info);
const fields = Object.entries(fieldMap.posts)
// check if field doesn't have subselections
.filter(([_, selections]) => !Object.keys(selections).length)
.map(([key]) => key);
const relations = Object.entries(fieldMap.posts)
// check if field has subselections
.filter(([_, selections]) => !!Object.keys(selections).length)
.map(([key]) => key);
return postsRepository.find({}, { populate: relations, fields });
},
},
Post: {
// fallback field resolver
author(post, { usersRepository }) {
if (post.author) {
// author was already fetched in query with KnexJS
return post.author;
}
// we still need to fetch the author from the DB
return usersRepository.findOne({ id: post.authorId });
},
},
};
note

This is the barebones method of using field maps to get columns and relations to SELECT and populate. See Resolving Selections For Use With ORMs for a more declarative approach.

Playground#

QueryLive

Code

import { resolveFieldMap } from "@jenyus-org/graphql-utils";
const resolvers = {
Query: {
posts(_, __, ___, info) {
const fieldMap = resolveFieldMap(info);
console.log(fieldMap);
},
},
};

Output

{
"posts": {
"id": {},
"title": {},
"body": {},
"author": {
"id": {},
"username": {},
"firstName": {},
"lastName": {}
}
}
}