npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

graphql-fields-list

v2.3.0

Published

Extracts and returns list of fields requested from graphql resolver info object

Downloads

64,570

Readme

graphql-fields-list

Build Status codebeat badge Coverage Status Known Vulnerabilities License

Add-on to work with GraphQLResolveInfo which helps to extract requested fields list for a particular object resolver. This helps to bypass requested fields data to underlying services or data sources to extract only those minimal parts of data which was requested by end-user.

TypeScript Included!!!

Install

npm i graphql-fields-list

With JavaScript:

const { fieldsList, fieldsMap } = require('graphql-fields-list');

With TypeScript:

import { fieldsList, fieldsMap } from 'graphql-fields-list';

Simplest Usage Examples

For the following query :

{
  post { # post: [Post]
    id
    author: {
      id
      firstName
      lastName
    }
  }
}
resolve(source, args, context, info) { // resolver of Post.author, 
  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 };
}

// or, if there is high-level resolver does the work:

resolve(source, args, context, info) { // resolver of Post
  console.log(fieldsList(info));       // [ 'id', 'author' ]
  console.log(fieldsMap(info));        // { id: false, author: { id: false, firstName: false, lastName: false } }
  console.log(fieldsProjection(info)); // { id: 1, 'author.id': 1, 'author.firstName': 1, 'author.lastName': 1 };
}

Breaking Changes

Since version 2.0.0 there is breaking change in fieldsMap() function interface now it relies on the same options object as was defined for fieldsList() instead of bypassing separate arguments. You will need to change your code if fieldsMap being used.

For example, if there was a usage of path and withDirecives arguments, like:

fieldsMap(info, 'users.edges.node', false);

it should be changed to:

fieldsMap(info, { path: 'users.edges.node', withDirectives: false });

Advanced Usage

Let's assume we have the following GraphQL schema:

interface Node {
  id: ID!
}
type PageInfo {
  hasNextPage: Boolean!
  hasPreviousPage: Boolean!
  startCursor: String
  endCursor: String
}
type Query {
  node(id: ID!): Node
  viewer: Viewer
}
type User implements Node {
  id: ID!
  firstName: String
  lastName: String
  phoneNumber: String
  email: String
}
type UserConnection {
  pageInfo: PageInfo!
  edges: [UserEdge]
}
type UserEdge {
  node: User
  cursor: String!
}
type Viewer {
  users(
    after: String,
    first: Int,
    before: String,
    last: Int
  ): UserConnection
}

And using the query:

query UserNames query {
    viewer {
        users {
            pageInfo {
                startCursor
                endCursor
            }
            edges {
                cursor
                node {
                    id
                    firstName
                    lastName
                }
            }
        }
    }
}

Our goal is to extract and return ONLY id, firstName and lastName fields from the user data. To achieve that we would need to bypass required fields information to underlying service or database, for example, let's assume we want to select that kind of data from mongodb.

In this case we will need to implement a resolver which will fetch only requested fields from our database like this:

const { connectionFromArray } from 'graphql-relay';
const { fieldsList } = require('graphql-fields-list');
// ... assuming we implement resolver on 'viewer' node:
async resolve(src, args, context, info) {
    // we want to get a clue which user data fields are requested, so:
    const fields = fieldsList(info, { path: 'users.edges.node' });
    // RESULT: fields = ['id', 'firstName', 'lastName']
    // Now we can fetch from mongodb only required part of the data
    // instead of fetching entire user data document (assuming
    // userDb is initialized model of mongoose):
    const users = await userDb.find().select(fields.join(' ')).exec();
    return { viewer: { users: connectionFromArray(users, args) } };
}

In the example above we assume our user model in database contains the same field names as defined by a graphql request. BTW, in a real world, there could be a need to re-map field names from a graphql query to some different names stored in a database. For example, we would need to use automatically created _id field in mongodb as id field in a graphql request. This can be easily achieved specifying a transform map option:

const fields = fieldsList(info, {
    path: 'users.edges.node',
    transform: { id: '_id' },
});
// RESULT: fields = ['_id', 'firstName', 'lastName']

By the way, in some particular cases there could be a need to retrieve a whole fields name hierarchy from a graphql request. This could be achieved using fieldsMap function:

const { fieldsMap } = require('graphql-fields-list');
// ... inside the resolver as we did above:
const map = fieldsMap(info);
/*
RESULT:
map = {
  users: {
    pageInfo: {
      startCursor: false,
      endCursor: false
    },
    edges: {
      cursor: false,
      node: {
        id: false,
        firstName: false,
        lastName: false
      }
    }
  }
}
*/

Function fieldsMap also accepts same optional arguments as fieldsList:

const map = fieldsMap(info, { path: 'users.pageInfo' });
/*
RESULT:
map = {
  startCursor: false,
  endCursor: false
}
*/

For leafs of the fields tree it will return false value, which is usable when you need to detect that the end of a tree branch is reached during traversal.

Both fieldsMap and fieldsList work as expected with graphql query fragmentation, so can be safely used within any possible queries.

Since version 1.1.0 it also supports @skip and @include directives in queries. This is enabled by default. If you need to disable directives support for some reason it may be turned off using withDirectives = false option correspondingly:

fieldsList(info, { withDirectives: false });
fieldsMap(info, { withDirectives: false });

Please, note, currently fieldsMap accepts transform option argument, but DOES NOT USE IT for transformations. This function will return always the map of the actual query fields. All transformations accepted only by fieldsList and fieldsProjection functions!

Since version 2.0.0

In some cases it could be useful to operate with fields projections instead of mapping object. For example, projection could be used with MongoDB queries. To extract fields projection object from GraphQLResoleInfo you can utilize fieldsProjection() function:

const projection = fieldsProjection(info, { path: 'users.edges.node' });
/*
RESULT:
projection = {
  id: 1,
  firstName: 1,
  lastName: 1,
  phoneNumber: 1,
  email: 1,
  address: 1,
}
*/

Projections use dot-notation for a fields and always returned as a flat object:

const projection = fieldsProjection(info, { path: 'users.edges' });
/*
RESULT:
projection = {
  'node.id': 1,
  'node.firstName': 1,
  'node.lastName': 1,
  'node.phoneNumber': 1,
  'node.email': 1,
  'node.address': 1,
}
*/

Projections also accepts keepParentField option, which should return the parents included in the object not only the leaves.

const projection = fieldsProjection(info, {
    path: 'users',
    keepParentField: true,
});
/*
RESULT:
projection = {
  'edges': 1,                 // parent node
  'edges.node': 1,            // parent node
  'pageInfo': 1,              // parent node
  'pageInfo.startCursor': 1,
  'pageInfo.endCursor': 1,
  'pageInfo.hasNextPage': 1,
  'edges.node.id': 1,
  'edges.node.firstName': 1,
  'edges.node.lastName': 1,
  'edges.node.phoneNumber': 1,
  'edges.node.email': 1,
  'edges.node.address': 1,
}
*/

Projections also accepts transform option, which should be a mapping object between projections paths:

const projection = fieldsProjection(info, {
    path: 'users.edges',
    transform: {
        'node.id': 'node._id',
        'node.firstName': 'node.given_name',
        'node.lastName': 'node.family_name',
    },
});
/*
RESULT:
projection = {
  'node._id': 1,
  'node.given_name': 1,
  'node.family_name': 1,
  'node.phoneNumber': 1,
  'node.email': 1,
  'node.address': 1,
}
*/

Since version 2.1.0

It supports skip option to filter output of fieldsList(), fieldsMap() and fieldsProjection() functions.

See motivation

Skip option accepts an array of field projections to skip. It allows usage of wildcard symbol * within field names. Please, note, that skip occurs before transformations, so it should reflect original field names, transformations would be applied after skip is done.

Typical usage as:

const map = fieldsMap(info, { skip: [
    'users.pageInfo.*',
    'users.edges.node.email',
    'users.edges.node.address',
    'users.edges.node.*Name',
]});
/*
RESULT:
map = {
  users: {
    edges: {
      node: {
        id: false,
        phoneNumber: false,
      },
    },
  },
}
*/
const projection = fieldsProjection(info, {
   skip: [
       'users.pageInfo.*',
       'users.edges.node.email',
       'users.edges.node.address',
       'users.edges.node.*Name',
   ],
   transform: {
       'users.edges.node.id': 'users.edges.node._id',
   },
});
/*
RESULT:
projection = {
 'users.edges.node._id': 1,
 'users.edges.node.phoneNumber': 1,
};
*/

Frequent Questions and Answers

Q1. Can we exclude __typename from fieldsList?

const some = fieldsList(info)
// some output
[ 'id', 'name', '__typename' ]

A1. Usually this problem occurs with using Apollo clients. Sure, you can overcome this with use of skip option:

const some = fieldsList(info, { skip: ['__*'] })

This is exactly the case, why skip option is created for.

License

ISC Licence