Research
Security News
Malicious npm Packages Inject SSH Backdoors via Typosquatted Libraries
Socket’s threat research team has detected six malicious npm packages typosquatting popular libraries to insert SSH backdoors.
graphql-norm
Advanced tools
Normalization and denormalization of GraphQL responses
npm install graphql-norm --save
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 have the same ID and are of the same GraphQL type so they are logically the same object. 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.
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.
You can also run the below example live at stackblitz.
import { normalize, denormalize, merge } from "graphql-norm";
import { request } from "graphql-request";
import { parse } from "graphql";
// A plain JS object to hold the normalized responses
let cache = {};
// This query will be fetched from the server
const query = `
query GetCountry($code: String!) {
country(code: $code) {
__typename code name
continent {__typename code name}
languages {__typename code name}
}
}`;
const queryDoc = parse(query);
const queryVars = { code: "SE" };
request("https://countries.trevorblades.com/graphql", query, queryVars).then(
data => {
console.log("data", JSON.stringify(data));
/*
{
"country": {
"__typename": "Country",
"code": "SE",
"name": "Sweden",
"continent": {"__typename": "Continent", "code": "EU", "name": "Europe"},
"languages": [{"__typename": "Language", "code": "sv", "name": "Swedish"}]
}
}
*/
// Function to find normalized key for each object in response data
const getKey = obj =>
obj.code && obj.__typename && `${obj.__typename}:${obj.code}`;
// Normalize the response data
const normMap = normalize(queryDoc, queryVars, data, getKey);
// In the normalized data, an ID was assigned to each object.
// References between objects are now using these IDs.
console.log("normMap", JSON.stringify(normMap));
/*
{
"ROOT_QUERY": {"country({\"code\":\"SE\"})": "Country:SE"},
"Country:SE": {
"__typename": "Country",
"code": "SE",
"name": "Sweden",
"languages": ["Language:sv"],
"continent": "Continent:EU"
},
"Language:sv": {"__typename": "Language", "code": "sv", "name": "Swedish"},
"Continent:EU": {"__typename": "Continent", "code": "EU", "name": "Europe"}
}
*/
// Merge the normalized response into the cache
cache = merge(cache, normMap);
// Now we can now use denormalize to read a query from the cache
const query2 = `
query GetCountry2($code: String!) {
country(code: $code) {__typename code name}
}`;
const query2Doc = parse(query2);
const denormResult = denormalize(query2Doc, { code: "SE" }, cache);
const setToJSON = (k, v) => (v instanceof Set ? Array.from(v) : v);
console.log("denormResult", JSON.stringify(denormResult, setToJSON));
/*
{
"data": {"country": {"__typename": "Country","code": "SE","name": "Sweden"}},
"fields": {
"ROOT_QUERY": ["country({\"code\":\"SE\"})"],
"Country:SE": ["__typename", "code", "name"]
}
}
*/
}
);
const normMap = normalize(query, variables, data, getObjectId, resolveType);
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.
__typename
and id
and combines them into a __typename:id
string will be used. If this function returns a falsy value (eg. undefined), a fallback ID will be used. Some objects may be value objects that have no ID and in that case it is OK to return falsy. The fallback ID will use the closest parent with an ID as a base (or ROOT_QUERY if there is no parent with ID).... on Bar
should only apply to objects of type Bar
. This becomes extra useful when graphql union types are used. The default implementation will look for __typename
of the object.This function returns an object that is a map of keys and normalized objects.
const denormResult = denormalize(query, variables, normMap);
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 undefined
will be returned.
normalize()
and/or merge()
.normalize()
above.This function returns an object with information about the denormalization result. The following properties are available on the returned object:
undefined
is the query could not be completely fulfilled from the data in normMap
.Set
of used fields. This can be useful for tracking which key/fields will affect this query data. If an tuple of this object and the data is stored, each time a new normalized result is merged a cache we can check if the new normalized data being merged contains any of the keys/fields of this query then it is affected by the merge, otherwise not. This is similar to the approach used by relay for tracking changes. If the query could not be fulfilled from cache, data will be undefined
and fields
will contain the first field that could not be resolved.const normMap = merge(normMap, newNormMap);
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.
This function returns an object which is the merged normalized map. It has the same structure as the passed in objects but the keys/values from both of them.
This project is developed using typescript and typescript types are distributed with the package.
Node version >=12.6.0 is needed for development.
To execute the tests run yarn test
.
yarn version --patch
yarn version --minor
yarn version --major
FAQs
Normalization and denormalization of GraphQL responses
We found that graphql-norm demonstrated a not healthy version release cadence and project activity because the last version was released a year ago. It has 9 open source maintainers collaborating on the project.
Did you know?
Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.
Research
Security News
Socket’s threat research team has detected six malicious npm packages typosquatting popular libraries to insert SSH backdoors.
Security News
MITRE's 2024 CWE Top 25 highlights critical software vulnerabilities like XSS, SQL Injection, and CSRF, reflecting shifts due to a refined ranking methodology.
Security News
In this segment of the Risky Business podcast, Feross Aboukhadijeh and Patrick Gray discuss the challenges of tracking malware discovered in open source softare.