graphql-norm
Normalization and denormalization of GraphQL responses
How to install
npm install graphql-norm --save
Introduction
Responses from graphql servers may contain the same logical object several times. Consider for example a response from a blog server that contains a person object both as an author and a commenter. Both person objects are of the same GraphQL type and thus have the same fields and ID. However, since they appear in two different parts of the response they need to be duplicated. When we want to store several GraphQL responsese the problem of duplication amplifies, as many respones may contain the same object. When we later want to update an object, it can be difficult to find all the places where the update needs to happen because there are multiple copies of the same logical object. This package solves these problems by using normalization and denormalization.
A basic description of normalization (in this context) is that it takes a tree and flattens it to a map where each object will be assigned an unique ID which is used as the key in the map. Any references that an object holds to other objects will be exhanged to an ID instead of an object reference. The process of denormalizaton goes the other way around, starting with a map and producing a tree. The normalizr library does a good job of explaining this. In fact, this package is very similar to normalizr, but it was specifically designed to work with GraphQL so it does not require hand-coded normalization schemas. Instead it uses GraphQL queries to determine how to normalize and denormalize the data.
Normalization and denormalization is useful for a number of scenarios but the main usage is probably to store and update a client-side GraphQL cache without any duplication problems. For example, Relay and Apollo use this approach for their caches. So the main use-case for this library is probably to build your own client-side cache where you get full control of the caching without loosing the benefit of normalization.
Goal
The goal of the package is only to perform normalization and denormalization of graphql responses. Providing a complete caching solution is an explicit non-goal of this package. However this package can be a building block in a normalized GraphQL caching solution.
Features
- Turn any graphql response into a flat (normalized) object map
- Build a response for any grapqhl query from the normalized object map (denormalize)
- Merge normalized object maps to build a larger map (eg. a cache)
- Full GraphQL syntax support (including variables, alias, @skip, @include)
- Optimized for run-time speed
Example usage
import { normalize, denormalize, merge } from "graphql-norm";
import { request } from "graphql-request";
let cache = {};
const query = gql`
query TestQuery {
posts {
id
__typename
title
author {
id
__typename
name
}
comments {
id
__typename
commenter {
id
__typename
name
}
}
}
}
`;
const response = request(query);
const normalizedResponse = normalize(query, {}, response);
cache = merge(cache, normalizedResponse);
const cachedResponse = denormalize(query, {}, cache);
API
normalize()
The normalize() function takes a GraphQL query with associated variables, and data from a GraphQL response. From those inputs it produces a normalized object map which is returned as a plain JS object. Each field in the query becomes a field in the normalized version of the object. If the field has variables they are included in the field name to make them unique. If the object has nested child objects they are exhanged for the ID of the nested object, and the nested objects becomes part of the normalized object map. This happens recursively until there are no nested objects left.
normalize(
query: GraphQL.DocumentNode,
variables: { [key: string]: any } | undefined,
response: { data: any },
getObjectId: (object: any) => string
): { [key: string]: any }
denormalize()
The denormalize() function takes a GraphQL query with associated variables, and a normalized object map (as returned by normalize()). From those inputs it produces the data for a GraphQL JSON response. Note that the GraphQL query can be any query, it does not have to be one that was previously normalized. If the response cannot be fully created from the normalized object map then partial
will be set to true
.
export function denormalize(
query: GraphQL.DocumentNode,
variables: { [key: string]: any } | undefined,
cache: { [key: string]: any },
staleMap: { [field: string]: true | undefined } | undefined
): {
response: { data: any } | undefined;
partial: boolean;
stale: boolean;
};
merge()
When you normalize the response of a query you probably want to merge the resulting normalized object map into a another, large normalized object map that is held by your application. Since the normalized object map is just a JS object you can do this merge any way you want but the merge() function is provided an optimized convenience to do the merging.
Related packages
How to develop
Node version >=12.6.0 is needed for development.
To execute the tests run yarn test
.
How to publish
yarn version --patch
yarn version --minor
yarn version --major