Get list of requested keys in NestJS/GraphQL request

Viewed 4620

I am just fiddling around trying to understand, thus my types are not exact.

@Resolver()
export class ProductsResolver {
    @Query(() => [Product])
    async products() {
        return [{
            id: 55,
            name: 'Moonshine',
            storeSupplies: {
                London: 25,
                Berlin: 0,
                Monaco: 3,
            },
        }];
    }
}

If I request data with query bellow

{
    products{
      id,
      name,
    }
}

I want async carriers() to receive ['id', 'name']. I want to skip getting of storeSupplies as it might be an expensive SQL call.


I am new to GraphQL, I might have missed something obvious, or even whole patterns. Thanks in advance.

4 Answers


Basically you can seperate StoreSupplies queries, to make sure not to get them when query on the products.
You can also get the requested keys in your resolver, then query based on them. In order to do that, you can define a parameter decorator like this:

import { createParamDecorator } from '@nestjs/common';

export const Info = createParamDecorator(
  (data, [root, args, ctx, info]) => info,
);

Then use it in your resolver like this:

  @UseGuards(GqlAuthGuard)
  @Query(returns => UserType)
  async getMe(@CurrentUser() user: User, @Info() info): Promise<User> {
    console.log(
      info.fieldNodes[0].selectionSet.selections.map(item => item.name.value),
    );
    return user;
  }

For example, when you run this query

{
  getMe{
    id
    email
    roles
  }
}

The console.log output is:

[ 'id', 'email', 'roles' ]

Another option is to directly use the @Info decorator provided by NestJS, as found here: https://docs.nestjs.com/graphql/resolvers-map#decorators

It could look something like this:

@Resolver()
export class ProductsResolver {
    @Query(() => [Product])
    async products(
    @Info() info
    ) {
        // Method 1 thanks to @pooya-haratian.
        // Update: use this method; read below article to understand why.
        let keys = info.fieldNodes[0].selectionSet.selections.map(item => item.name.value);
        // Method 2 by me, but I'm not sure which method is best.
        // Update: don't use this; read below article to understand why.
        let keys = info.operation.selectionSet.selections[0].selectionSet.selections.map(field => field.name.value);
        return keys;
    }
}

Update: After reading this article on GraphQL Server Basics: Demystifying the info Argument in GraphQL Resolvers, I have learned that fieldNodes is an excerpt of the Abstract Syntax Tree (AST), while operation is the AST of the entire query.

As for why it's safe to select the first object in the array of fieldNodes (fieldNodes[0]), it is because the excerpt starts at the current field, as opposed to the root of the query.

Hence, @pooya-haratian's method was correct. I simply added the elaboration and used NestJS's decorator (@Info).

Basically, you can use @Info https://docs.nestjs.com/graphql/resolvers#graphql-argument-decorators decorator from NestJs which is returning an info parameter from regular apollo resolver.

This decorator injects parsed GraphQL query as AST and allows user to create more complex resolvers.

Working with AST is not straightforward and easy because you need to handle all query types by yourself (fragments, aliases, directives and etc) But fortunately, there are some libs on the market that make all heavy lifting under the hood.

@jenyus-org/graphql-utils

https://www.npmjs.com/package/@jenyus-org/graphql-utils

This also has pretty useful Decorators for NestJS:

https://www.npmjs.com/package/@jenyus-org/nestjs-graphql-utils

CODE

@Query(() => [PostObject])
async posts(
  @FieldMap() fieldMap: FieldMap,
) {
  console.log(fieldMap);
}

OUTPUT

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

graphql-fields-list

https://www.npmjs.com/package/graphql-fields-list

Example in NestJS:

{
  post { # post: [Post]
    id
    author: {
      id
      firstName
      lastName
    }
  }
}
import { fieldsList, fieldsMap } from 'graphql-fields-list';
import { Query, Info } from '@nestjs/graphql';

@Query(() => [Post])
async post(
  @Info() info,
) {
  console.log(fieldsList(info));       // [ 'id', 'firstName', 'lastName' ]
  console.log(fieldsMap(info));        // { id: false, firstName: false, lastName: false }
  console.log(fieldsProjection(info)); // { id: 1, firstName: 1, lastName: 1 };
}

Other similar libs

https://www.npmjs.com/package/graphql-parse-resolve-info

https://github.com/robrichard/graphql-fields

After couple tries I also managed to create my own solution which doesn't require external libraries.

field-map.decorator.ts

import { createParamDecorator, ExecutionContext } from '@nestjs/common';
import { GqlExecutionContext } from '@nestjs/graphql';

const getNodeData = (node) => {
  const { selectionSet } = node || {};

  let fields = null;
  if (!!selectionSet) {
    fields = {};
    selectionSet.selections.forEach((selection) => {
      const name = selection.name.value;
      fields[name] = getNodeData(selection);
    });
  }

  return fields;
};

export const FieldMap = createParamDecorator((_, ctx: ExecutionContext) => {
  const gqlCtx = GqlExecutionContext.create(ctx);
  const info = gqlCtx.getInfo();

  const node = info.fieldNodes[0];
  return getNodeData(node);
});

example.resolver.ts

@Resolver()
export class ExampleResolver {
  @Query()
  example(@FieldMap() fieldMap) {
    console.log(fieldMap);
  }
}

Query

example {
  id
  name
  description
  child1 {
    id
    name
  }
  child2 {
    id
    name
    value
  }
}

Console output:

{
  id: null,
  name: null,
  description: null,
  child1: { id: null, name: null },
  child2: { id: null, name: null, value: null }
}
Related