All Projects → monojack → Graphql Normalizr

monojack / Graphql Normalizr

Licence: mit
Normalize GraphQL responses for persisting in the client cache/state

Programming Languages

javascript
184084 projects - #8 most used programming language

Projects that are alternatives of or similar to Graphql Normalizr

Loona
🌕 Application State Management done with GraphQL
Stars: ✭ 270 (+54.29%)
Mutual labels:  graphql, state-management
Jss
Official repo of Sitecore JavaScript Services
Stars: ✭ 173 (-1.14%)
Mutual labels:  graphql
Spaceace
A fancy immutable storage library for JavaScript
Stars: ✭ 167 (-4.57%)
Mutual labels:  state-management
Graphql Rate Limit
🚦 Fixed window rate limiting middleware for GraphQL. Use to limit repeated requests to queries and mutations.
Stars: ✭ 171 (-2.29%)
Mutual labels:  graphql
Graphql Toolkit
A set of utils for faster development of GraphQL tools
Stars: ✭ 169 (-3.43%)
Mutual labels:  graphql
Graphql Java Servlet
Servlet endpoint for GraphQL Java
Stars: ✭ 172 (-1.71%)
Mutual labels:  graphql
Hgplaceholders
Nice library to show placeholders and Empty States for any UITableView/UICollectionView in your project
Stars: ✭ 2,048 (+1070.29%)
Mutual labels:  state-management
Wp Graphql Jwt Authentication
Authentication for WPGraphQL using JWT (JSON Web Tokens)
Stars: ✭ 172 (-1.71%)
Mutual labels:  graphql
Graphene Django Subscriptions
This package adds support to Subscription's requests and its integration with websockets using Channels package.
Stars: ✭ 173 (-1.14%)
Mutual labels:  graphql
Awesome Graphql
Awesome list of GraphQL
Stars: ✭ 13,020 (+7340%)
Mutual labels:  graphql
Swifthub
GitHub iOS client in RxSwift and MVVM-C clean architecture
Stars: ✭ 2,330 (+1231.43%)
Mutual labels:  graphql
Api.xiaoduyu.com
🐟小度鱼 - 年轻人的交流社区 https://www.xiaoduyu.com
Stars: ✭ 168 (-4%)
Mutual labels:  graphql
Federation
Apollo Federation
Stars: ✭ 171 (-2.29%)
Mutual labels:  graphql
Graphql Tag.macro
Babel Macro for graphql-tag
Stars: ✭ 168 (-4%)
Mutual labels:  graphql
Space Cloud
Open source Firebase + Heroku to develop, scale and secure serverless apps on Kubernetes
Stars: ✭ 3,323 (+1798.86%)
Mutual labels:  graphql
Wxapp Graphql
📱A GraphQL client for 微信小程序
Stars: ✭ 167 (-4.57%)
Mutual labels:  graphql
Graphql Errors
Simple error handler for GraphQL Ruby ❗️
Stars: ✭ 170 (-2.86%)
Mutual labels:  graphql
Graphql S2s
Add GraphQL Schema support for type inheritance, generic typing, metadata decoration. Transpile the enriched GraphQL string schema into the standard string schema understood by graphql.js and the Apollo server client.
Stars: ✭ 171 (-2.29%)
Mutual labels:  graphql
Scalable React Typescript Boilerplate
⭐️ Scalable micro-framework featuring React and TypeScript
Stars: ✭ 174 (-0.57%)
Mutual labels:  graphql
Nestjs Microservice Architecture
A reference architecture for building microservices with Nest
Stars: ✭ 174 (-0.57%)
Mutual labels:  graphql

graphql-normalizr

publish npm version npm downloads minified size

Normalize GraphQL responses for persisting in the client cache/state.

Not related, in any way, to normalizr, just shamelessly piggybacking on its popularity. Also, "normalizEr" is taken...

TL;DR: Transforms:

{
  "data": {
    "findUser": [
      {
        "__typename": "User",
        "id": "5a6efb94b0e8c36f99fba013",
        "email": "[email protected]",
        "posts": [
          {
            "__typename": "BlogPost",
            "id": "5a6efb94b0e8c36f99fba016",
            "title": "Dolorem voluptatem molestiae",
            "comments": [
              {
                "__typename": "Comment",
                "id": "5a6efb94b0e8c36f99fba019",
                "message": "Alias quod est voluptatibus aut quis sunt aut numquam."
              },
              {
                "__typename": "Comment",
                "id": "5a6efb94b0e8c36f99fba01b",
                "message": "Harum quia asperiores nemo."
              },
              {
                "__typename": "Comment",
                "id": "5a6efb94b0e8c36f99fba01c",
                "message": "Vel veniam consectetur laborum."
              },
              {
                "__typename": "Comment",
                "id": "5a6efb94b0e8c36f99fba01e",
                "message": "Possimus beatae vero recusandae beatae quas ut commodi laboriosam."
              }
            ]
          }
        ]
      }
    ]
  }
}

into:

{
  "comments": {
    "5a6efb94b0e8c36f99fba019": {
      "id": "5a6efb94b0e8c36f99fba019",
      "message": "Alias quod est voluptatibus aut quis sunt aut numquam."
    },
    "5a6efb94b0e8c36f99fba01b": {
      "id": "5a6efb94b0e8c36f99fba01b",
      "message": "Harum quia asperiores nemo."
    },
    "5a6efb94b0e8c36f99fba01c": {
      "id": "5a6efb94b0e8c36f99fba01c",
      "message": "Vel veniam consectetur laborum."
    },
    "5a6efb94b0e8c36f99fba01e": {
      "id": "5a6efb94b0e8c36f99fba01e",
      "message": "Possimus beatae vero recusandae beatae quas ut commodi laboriosam."
    }
  },
  "blogPosts": {
    "5a6efb94b0e8c36f99fba016": {
      "id": "5a6efb94b0e8c36f99fba016",
      "title": "Dolorem voluptatem molestiae",
      "comments": [
        "5a6efb94b0e8c36f99fba019",
        "5a6efb94b0e8c36f99fba01b",
        "5a6efb94b0e8c36f99fba01c",
        "5a6efb94b0e8c36f99fba01e"
      ]
    }
  },
  "users": {
    "5a6efb94b0e8c36f99fba013": {
      "id": "5a6efb94b0e8c36f99fba013",
      "email": "[email protected]",
      "posts": ["5a6efb94b0e8c36f99fba016"]
    }
  }
}

Motivation

We all love GraphQL and we want to use it. There are tons of libraries and clients out there that help us do that with ease, but there is still one problem... How do you persist that data?

Yes, everything is all great when the response mirrors the exact structure we asked for, but we don't want to cache it that way, do we? We probably want a normalized version of that data which we can persist to our store and read/modify it efficiently. Flux or Redux stores work best with normalized data and there are also GraphQL clients you can use to execute queries on the local cache/state (blips or apollo-link-state), in which case, we definitely need to persist normalized data.

GraphQLNormalizr is simple, fast, light-weight and it provides all the tools needed to do just that, the only requirement is that you include the id and __typename fields for all the nodes (but it can do that for you if you're too lazy or you want to keep your sources thin).

Table of contents

Installation

npm install graphql-normalizr

API by example

The GraphQLNormalizr constructor function returns an object containing 3 methods:

  1. parse
  2. addRequiredFields
  3. normalize

Depending on how you write your queries, you may or may not use parse or addRequiredFields, but normalize is the method that you will transform the GraphQL response. As you've probably seen from the TL;DR, all response nodes must contain the __typename and id fields. __typename is a GraphQL meta field and the id key may be customized when creating the GraphQLNormalizr client.

If your queries already ask for id and __typename there's no need to use parse or addRequiredFields. Otherwise, parse will take care of transforming your GraphQL source into a Document and add the __typename and id fields where needed. In case you already use a different parser, or only have access to the Document you may use addRequiredFields on the Document to add the __typename and id fields

GraphQLNormalizr

import { GraphQLNormalizr } from 'graphql-normalizr'

// const config = ...
const normalizer = new GraphQLNormalizr(config)

config: optional - the configuration object containing information for instantiating the client. it takes the following props:

idKey

String

Default is "id". Configures a custom id key for the client. Use this if your resource identifiers are found under a different key name ('_id', 'key', 'uid' etc).

Consider the following GraphQL response:

const response = {
  data: {
    findUser: {
      __typename: 'User',
      uid: '5a6efb94b0e8c36f99fba013',
      email: '[email protected]',
    },
  },
}

Normalize the data with our custom id key:

// using destructuring to get the `normalize` method of the client
const { normalize } = new GraphQLNormalizr({ idKey: 'uid' })
normalize(response)
// =>
// {
//  users: {
//    '5a6efb94b0e8c36f99fba013' : {
//      uid: '5a6efb94b0e8c36f99fba013',
//      email: '[email protected]'
//    }
//  }
// }
useConnections

Boolean

Default is false. If you are using GraphQL connections with edges and nodes, set this flag to true otherwise you'll get a warning and the normalization won't work.

NOTE: The connections implementation needs to be according to the specification

const response = {
  data: {
    findUser: {
      __typename: 'User',
      id: '5a6efb94b0e8c36f99fba013',
      email: '[email protected]',
      friends: {
        __typename: 'FriendsConnection',
        totalCount: 3,
        edges: [
          {
            node: {
              __typename: 'User',
              id: '5a6cf127c2b20834f6551481',
              email: '[email protected]',
            },
            cursor: 'Y3Vyc29yMg==',
          },
          {
            node: {
              __typename: 'User',
              id: '5a6cf127c2b20834f6551482',
              email: '[email protected]',
            },
            cursor: 'Y3Vyc29yMw==',
          },
        ],
        pageInfo: {
          endCursor: 'Y3Vyc29yMw==',
          hasNextPage: false,
        },
      },
    },
  },
}

const { normalize } = new GraphQLNormalizr({
  useConnections: true,
})

normalize(response)
// =>
// {
//   users: {
//     '5a6efb94b0e8c36f99fba013': {
//       id: '5a6efb94b0e8c36f99fba013',
//       email: '[email protected]',
//       friends: ['5a6cf127c2b20834f6551481', '5a6cf127c2b20834f6551482'],
//     },
//     '5a6cf127c2b20834f6551481': {
//       id: '5a6cf127c2b20834f6551481',
//       email: '[email protected]',
//     },
//     '5a6cf127c2b20834f6551482': {
//       id: '5a6cf127c2b20834f6551482',
//       email: '[email protected]',
//     },
//   },
// }
typeMap

Object

By default the entity name will be the plural form of the type name, converted to camel case, (PrimaryAddress type will be stored under the primaryAddresses key). Use this option to provide specific entity names for some/all Types, or try the plural and casing options to derive the entity names.

const response = {
  data: {
    findUser: {
      __typename: 'User',
      id: '5a6efb94b0e8c36f99fba013',
      email: '[email protected]',
    },
  },
}

const { normalize } = new GraphQLNormalizr({
  typeMap: { User: 'accounts' },
})
normalize(response)
// =>
// {
//  accounts: {
//    '5a6efb94b0e8c36f99fba013' : {
//      id: '5a6efb94b0e8c36f99fba013',
//      email: '[email protected]'
//    }
//  }
// }
exclude

Object

Prevent normalization of specified fields

const response = {
  data: {
    allUsers: [
      {
        __typename: 'User',
        id: '5a6efb94b0e8c36f99fba013',
        email: '[email protected]',
        preferences: null
        posts: [
          {
            __typename: 'BlogPost',
            id: '5a6cf127c2b20834f6551484',
            likes: 10,
            title: 'Sunt ut aut',
            tags: {},
          }
        ]
      },
      {
        __typename: 'User',
        id: '5a6efb94b0e8c36f99fba013',
        email: '[email protected]',
        preferences: { foo: 'apple', bar: 1,  baz: { a: 'b' }, quux: null, }
        posts: [
          {
            __typename: 'BlogPost',
            id: '5a6cf127c2b20834f6551485',
            likes: 23,
            title: 'Nesciunt esse',
            tags: [],
          }
        ]
      },
    ],
  },
}

Normalize the data excluding the preferences field on users and the tags field on blogPosts:

// using destructuring to get the `normalize` method of the client
const { normalize } = new GraphQLNormalizr({ exclude: { users: [ 'preferences' ], blogPosts: [ 'tags' ] } })
normalize(response)
// =>
// {
//   users: {
//     '5a6efb94b0e8c36f99fba013': {,
//       id: '5a6efb94b0e8c36f99fba013',
//       email: '[email protected]',
//       preferences: null
//     },
//     '5a6efb94b0e8c36f99fba013': {
//       id: '5a6efb94b0e8c36f99fba013',
//       email: '[email protected]',
//       preferences: { foo: 'apple', bar: 1,  baz: { a: 'b' }, quux: null, }
//     },
//   },
//   blogPosts: {
//     '5a6cf127c2b20834f6551484': {
//       id: '5a6cf127c2b20834f6551484',
//       likes: 10,
//       title: 'Sunt ut aut',
//       tags: {},
//     },
//     '5a6cf127c2b20834f6551485': {
//       id: '5a6cf127c2b20834f6551485',
//       likes: 23,
//       title: 'Nesciunt esse',
//       tags: [],
//     },
//   }
// }
plural

Boolean

Default is true. Set this to false if you don't want to pluralize entity names. Considering the previous response example:

const { normalize } = new GraphQLNormalizr({
  plural: false,
})
normalize(response)
// =>
// {
//  user: {
//    '5a6efb94b0e8c36f99fba013' : {
//      id: '5a6efb94b0e8c36f99fba013',
//      email: '[email protected]'
//    }
//  }
// }
casing

'lower'|'upper'|'camel'|'pascal'|'snake'|'kebab'

You can also specify the preferred casing for entity names. Again, consider the above response example.

// casing: 'lower'
// User => user

// casing: 'upper'
// User => USER

// casing: 'camel'
// PrimaryAddress => primaryAddress

// casing: 'pascal'
// PrimaryAddress => PrimaryAddress

// casing: 'snake'
// PrimaryAddress => primary_address

// casing: 'kebab'
// PrimaryAddress => primary-address

Combine plural and casing options to get the desired entity names

lists

Boolean

Default is false. All the data is stored in key/value pairs, for easy access. If you want to use arrays, for whatever reason, set this to true

For the same response object in our previous example:

const { normalize } = new GraphQLNormalizr({
  lists: true,
})
normalize(response)
// =>
// {
//  users: [
//    {
//      id: '5a6efb94b0e8c36f99fba013',
//      email: '[email protected]'
//    }
//  ]
// }
typenames

Boolean

Default is false. The normalized data will not contain the __typename field. Set this to true if you need to persist them.

const { normalize } = new GraphQLNormalizr({
  typenames: true,
})

normalize(response)
// =>
// {
//  users: {
//    '5a6efb94b0e8c36f99fba013' : {
//      __typename: 'User',
//      id: '5a6efb94b0e8c36f99fba013',
//      email: '[email protected]'
//    }
//  }
// }
typePointers

Boolean

Default is false. Enables explicit type pointers - instead of an array of only identifiers and having to figure out which collection they point to, it will return objects containing the identifier as well as the collection name. Works especially well with Union types and Interfaces.

const { normalize } = new GraphQLNormalizr({
  typePointers: true,
})

// ['5a6cf127c2b20834f655148a', '5a6cf127c2b20834f655148b', '5a6cf127c2b20834f655148c']
users: [
  {
    _id: '5a6cf127c2b20834f655148a', // '_id' or the specified key
    collection: 'members', // type Member
  },
  {
    _id: '5a6cf127c2b20834f655148b', // '_id' or the specified key
    collection: 'authors', // type Author
  },
  {
    _id: '5a6cf127c2b20834f655148c', // '_id' or the specified key
    collection: 'members', // type Member
  },
],
caching

Boolean

Default is false. The normalize method is pretty fast by itself, it does a single iteration and associates the values only for each response node and not for all the fields. Enable this if you think you'd be normalizing the same response multiple times, like when you're polling for data and it may not have changed.

const { normalize } = new GraphQLNormalizr({
  caching: true,
})

const normalized = normalize(response)
const cached = normalize(response)

cached === normalized // => true

parse

Turns a GraphQL source into a Document and adds the required fields where necessary.

// ...
import { GraphQLNormalizr } from 'graphql-normalizr'

const source = `{
  allUsers {
    email
    posts {
      title
      comments {
        message
      }
    }
  }
}`

const { parse } = new GraphQLNormalizr()

const query = parse(source) // will add `id` and `__typename` fields to all the nodes

// We can use the print method from `graphql` to see/use the updated source
const { print } = require('graphql')
print(query)
// =>
// `{
//  allUsers {
//    __typename
//    id
//    email
//    posts {
//      __typename
//      id
//      comments {
//        __typename
//        id
//        message
//      }
//    }
//  }
// }`

// ...

addRequiredFields

If you only have access to the Document, you can use the print method from graphql to get the source and parse it. But that may be expensive and you shouldn't have to print a document just to parse it again. addRequiredFields will add the id and __typename fields to that document, without the need of extracting its source.

// ...
import { GraphQLNormalizr } from 'graphql-normalizr'
import { allUsersQuery } from './queries'

const { addRequiredFields } = new GraphQLNormalizr()

const query = addRequiredFields(allUsersQuery)

// ...

normalize

The following is a full example where we use apollo-fetch to execute a query and then normalize it with GraphQLNormalizr

const { GraphQLNormalizr } = require('graphql-normalizr')
const { createApolloFetch } = require('apollo-fetch')

const uri = 'http://localhost:8080/graphql'
const fetch = createApolloFetch({ uri })

const source = `
  query {
    allUsers {
      ...userFields
    }
  }
  fragment userFields on User {
    email
    posts {
      title
      comments {
        message
      }
    }
  }
`

const { normalize, parse } = new GraphQLNormalizr()
const query = parse(source)

fetch({ query }).then(response => {
  const normalized = normalize(response)
  // persist the normalized data to our app state.
}).catch(...)

Migrating

from v1.x to v2.x

There aren't many breaking changes between v1.x and v2.x. In fact, there's only one and it's about how Type names get converted into entity names.

With the default configuration, v1.x will transfrom a PrimaryAddress type name into primaryaddresses entity name. With 2.x, the default configuiration will transform PrimaryAddress to primaryAddresses. The only difference is that now, it changes to camelcase instead of lowercase

If you don't want to change your code everywhere you are accessing entities, you can configure the way GraphQLNormalizr makes the transformation with the plural and casing options:

const { normalize } = new GraphQLNormalizr({
  plural: true, // true is the default value, so you can omit this
  casing: 'lower',
})

// this above configuration will change `PrimaryAddress` to `primaryaddresses`
Note that the project description data, including the texts, logos, images, and/or trademarks, for each open source project belongs to its rightful owner. If you wish to add or remove any projects, please contact us at [email protected].