Giter Site home page Giter Site logo

markshapiro / graphql-db-projection Goto Github PK

View Code? Open in Web Editor NEW
21.0 3.0 0.0 104 KB

Create projection of fields needed to fetch from db from Graphql query. Supports nested queries & custom field names.

License: MIT License

JavaScript 100.00%
graphql projection graphql-projection apollo apollo-server mongodb

graphql-db-projection's Introduction

graphql-db-projection

Given GraphQL query, creates db fields projection to fetch only fields that are required.
Supports lists, nested queries, fragments and inline fragments.

Installation

Install with yarn:

$ yarn add graphql-db-projection

or npm:

$ npm i -S graphql-db-projection

Setup

Prepare directives you intend to use:

import { ApolloProjector, IncludeAll, IgnoreField } from 'graphql-db-projection';

const typeDefs = gql`
  directive @proj(
    projection: String,
    projections: [String],
    nameInDB: String
  ) on FIELD_DEFINITION
  directive @all on FIELD_DEFINITION
  directive @ignore on FIELD_DEFINITION

  // ... your schemas
`;

// ...

// (you can also call the directives differently)
const server = new ApolloServer({
  typeDefs,
  resolvers,
  schemaDirectives: {
    proj: ApolloProjector,
    all: IncludeAll,
    ignore: IgnoreField
  }
});

Simple Example

Suppose we have User model:

const typeDefs = gql`
  type User {
    firstName: String
    lastName: String
    username: String
    address: Address
  }

  type Address {
    country: String
    city: String
    street: String
  }
`;

we can call makeProjection on last ASTs param to get projections mapping:

import makeProjection from 'graphql-db-projection';
// ...
const resolvers = {
  Query: {
    users: (obj, args, request, fieldASTs) => {
      const projection = makeProjection(fieldASTs);
      // ...
    },
  },
};

then the following query:

query ($id: String){
  user (id: $id){
    firstName
    address {
      city
      street
    }
  }
}

will produce projection:

{
  firstName: 1,
  address: {
    city: 1,
    street: 1
  }
}

now you can use it to project fields for db, for example for mongoDB:

import { toMongoProjection } from 'graphql-db-projection';

const resolvers = {
  Query: {
    users: (obj, args, request, fieldASTs) => {
      const projection = makeProjection(fieldASTs);
      const mongoProjection = toMongoProjection(projection)
      return db.collection('users').findOne(args.id, mongoProjection);
    }
  }
}

Include automatically all nested fields

To automatically include all nested fields of object use @all directive:

const typeDefs = gql`
  type User {
    username: String
    address: Address @all
  }
  
  type Address {
    city: String
    postalCode: String
  }
`;

now makeProjection result on query

query ($id: String){
  user (id: $id){
    firstName
    address {
      city
      street
    }
  }
}

will be:

{
  firstName: 1,
  address: 1
}

Ignore projection

To remove field from projection use @ignore directive:

const typeDefs = gql`
  type User {
    firstName: String
    lastName: String @ignore
    username: String
    address: Address @ignore
  }
`;

the result when requesting all fields will be just { firstName: 1, username: 1 }

Custom Projections

If resolve function of GraphQL field uses multiple DB fields to calculate the value, use @proj(projection: <field in db>) or @proj(projections: [<field in db>, ...]) to specify absolute paths of fields you need:

const typeDefs = gql`
  type User {

    // will add username to projection
    displayName: String @proj(projection: "username")

    // will add gender, firstName and lastName to projection
    fullName: String @proj(projections: ["gender", "firstName", "lastName"])
    
    address: Address
  }
  
  type Address {
    country: String
    fullAddress: @proj(projections: ["city", "street"])
  }
`;

const resolvers = {
  User: {
    // displayName is calculated from username in DB
    displayName: user => user.username,
    
    // fullName is calculated from gender, firstName, lastName in DB
    fullName: user => `${user.gender ? 'Mr.' : 'Mrs.'} ${user.firstName} ${user.lastName}`,
  },
  
  Address: {
    fullAddress: address => `${address.city} ${address.street}`,
  }
};

requesting all these fields in GraphQL query will result in projection:

{ 
  username: 1,
  gender: 1,
  firstName: 1,
  lastName: 1,
  addess: {
    country: 1,
    city: 1,
    street: 1
  }
}

Name of Field in DB called differently

Custom projections specify absolute paths inside nested project, but don't do recursion on the nested fields like you have by default. If only the name of field is called differently in DB but you want to project the nested fields recursively, use @proj(nameInDB: <field name in db>):

const typeDefs = gql`

  type User {
    username: String

    // stored as 'location' object in DB
    address: Address @proj(nameInDB: "location")
  }
  
  type Address {
    city: String
    postalCode: String
  }
`;

const resolvers = {
  Query: {
    users: (obj, args, request, fieldASTs) => {
      const projection = makeProjection(fieldASTs);

      // ...
    },
  },
  User: {
    address: user => user.location,
  },
};

requesting all these fields in GraphQL query will result in projection:

{
  username: 1,
  location: {
    city: 1,
    postalCode: 1
  }
}

Projection of subquery

If your subquery in GraphQL needs additional fetch from DB to join into parent object, remember that you can call makeProjection on fieldASTs argument inside subquery resolver function. Suppose we have array of Posts inside User:

const typeDefs = gql`
  type User {
    # suppose posts are located in different collection/table,
    # this is why we don't need to project that field in User
    posts: [PostType] @ignore
  }
`;

const resolvers = {
  User: {
    posts: (user, args, ctx, postsFieldASTs) => {

      // you can make new isolated projection only for User's Posts fetch
      // based on Post's GraphQL subquery
      const projectionOfPost = makeProjection(postsFieldASTs);
      const mongoProjection = toMongoProjection(projectionOfPost)
      return db.collection('posts')
          .find({ postedBy: user.id }, mongoProjection).toArray();
    },
  },
};

graphql-db-projection's People

Contributors

markshapiro avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar

graphql-db-projection's Issues

How to handle inline fragment?

Hello,

Do you have any ideas to handle inline fragments with your library in order to generate projections?

Thanks

Alvyre

Get first level of projection only

Hello!

first thank you for you work, it saves me a lot of time for my project!
I was wondering, how to parse ASTFields with your lib to get only first level fields (and transform multi level in first level). To be clear this is an example :

this is a request I made in GraphiQL

{
  My_item(id: "abcdefghijklmnoqlsnejngwd") {
    id
    name
    function {
      subfield1
      subfield2
    }
  }
}

by default I get this with your functions (makeProjection and toMongoProjection):
id: 1, name: 1, 'function.subfield1': 1, 'function.subfield2': 1

I would like to have something similar to this:
id: 1, name: 1, 'function: 1

I saw in your documentation that we can do that:

@proj(projections: [])

in our GraphQL schema, so I tried to make it work but this time I don't get my object at all so I can't make any request with its id as you suggest it here :

// but not posts as we explicitly omitted them because they are located in different collection

May be I didn't get something or did something wrong, IDK for the moment.

The only workaround I've found is to do something like:

function:            Function @proj(projection: "function")

in my Schema. It not seems to be a clean way to do it..
Do you have any ideas? thanks!

Projection of "InlineFragment"

Thanks for a great project.

My objective is to generate projection for Graphql Queries, containing InlineFragments.

Do you have any suggestions how to support this?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.