graphql-parse-resolve-info
Advanced tools
Comparing version 0.0.1-alpha2.0 to 0.0.1-alpha3.0
233
index.js
@@ -1,226 +0,11 @@ | ||
"use strict"; | ||
const assert = require("assert"); | ||
const { getArgumentValues } = require("graphql/execution/values"); | ||
const { getNamedType } = require("graphql"); | ||
const debug = require("debug")("graphql-parse-resolve-info"); | ||
// This script detects if you're running on Node v8 or above; if so it runs the | ||
// code directly, otherwise it falls back to the babel-compiled version | ||
// Originally based on https://github.com/tjmehta/graphql-parse-fields | ||
function getAlias(resolveInfo) { | ||
const asts = resolveInfo.fieldASTs || resolveInfo.fieldNodes; | ||
return asts.reduce(function(alias, val) { | ||
if (!alias) { | ||
if (val.kind === "Field") { | ||
alias = val.alias ? val.alias.value : val.name && val.name.value; | ||
} | ||
} | ||
return alias; | ||
}, null); | ||
if (process.versions.node.match(/^([89]|[1-9][0-9]+)\./)) { | ||
// Modern node, run verbatim | ||
module.exports = require("./src"); | ||
} else { | ||
// Older node, run compiled code | ||
require("babel-polyfill"); | ||
module.exports = require("./lib"); | ||
} | ||
function parseFields(resolveInfo, options = {}) { | ||
if (options.aliasOnly) { | ||
return getAlias(resolveInfo); | ||
} | ||
const fieldNodes = resolveInfo.fieldASTs || resolveInfo.fieldNodes; | ||
const { parentType } = resolveInfo; | ||
if (!fieldNodes) { | ||
throw new Error("No fieldNodes provided!"); | ||
} | ||
if (options.keepRoot == null) { | ||
options.keepRoot = false; | ||
} | ||
if (options.deep == null) { | ||
options.deep = true; | ||
} | ||
let tree = fieldTreeFromAST( | ||
fieldNodes, | ||
resolveInfo, | ||
undefined, | ||
options, | ||
parentType | ||
); | ||
if (!options.keepRoot) { | ||
const typeKey = firstKey(tree); | ||
tree = tree[typeKey]; | ||
const fieldKey = firstKey(tree); | ||
tree = tree[fieldKey]; | ||
} | ||
return tree; | ||
} | ||
function getFieldFromAST(ast, parentType) { | ||
if (ast.kind === "Field") { | ||
const fieldName = ast.name.value; | ||
return parentType.getFields()[fieldName]; | ||
} | ||
return; | ||
} | ||
let iNum = 1; | ||
function fieldTreeFromAST( | ||
inASTs, | ||
resolveInfo, | ||
initTree, | ||
options, | ||
parentType, | ||
depth = "" | ||
) { | ||
const instance = iNum++; | ||
debug( | ||
"%s[%d] Entering fieldTreeFromAST with parent type '%s'", | ||
depth, | ||
instance, | ||
parentType | ||
); | ||
let { fragments, variableValues } = resolveInfo; | ||
fragments = fragments || {}; | ||
initTree = initTree || {}; | ||
options = options || {}; | ||
const asts = Array.isArray(inASTs) ? inASTs : [inASTs]; | ||
initTree[parentType.name] = initTree[parentType.name] || {}; | ||
const outerDepth = depth; | ||
return asts.reduce(function(tree, val, idx) { | ||
const depth = `${outerDepth} `; | ||
const kind = val.kind; | ||
debug( | ||
"%s[%d] Processing AST %d of %d; kind = %s", | ||
depth, | ||
instance, | ||
idx + 1, | ||
asts.length, | ||
kind | ||
); | ||
const name = val.name && val.name.value; | ||
const isReserved = name && name.substr(0, 2) === "__"; | ||
if (kind === "Field" && !isReserved) { | ||
const alias = val.alias ? val.alias.value : name; | ||
debug("%s[%d] Field '%s' (alias = '%s')", depth, instance, name, alias); | ||
const field = getFieldFromAST(val, parentType); | ||
const fieldGqlType = getNamedType(field.type); | ||
const args = getArgumentValues(field, val, variableValues) || {}; | ||
if (!tree[parentType.name][alias]) { | ||
tree[parentType.name][alias] = { | ||
ast: val, | ||
alias, | ||
name, | ||
args, | ||
fieldsByTypeName: { | ||
[fieldGqlType.name]: {}, | ||
}, | ||
}; | ||
} | ||
if (val.selectionSet && options.deep) { | ||
debug("%s[%d] Recursing into subfields", depth, instance); | ||
fieldTreeFromAST( | ||
val.selectionSet.selections, | ||
resolveInfo, | ||
tree[parentType.name][alias].fieldsByTypeName, | ||
options, | ||
fieldGqlType, | ||
`${depth} ` | ||
); | ||
} else { | ||
// No fields to add | ||
debug("%s[%d] Exiting (no fields to add)", depth, instance); | ||
} | ||
} else if (kind === "FragmentSpread" && options.deep) { | ||
debug("%s[%d] Fragment spread '%s'", depth, instance, name); | ||
const fragment = fragments[name]; | ||
assert(fragment, 'unknown fragment "' + name + '"'); | ||
let fragmentType = parentType; | ||
if (fragment.typeCondition) { | ||
fragmentType = getType(resolveInfo, fragment.typeCondition); | ||
} | ||
if (fragmentType) { | ||
fieldTreeFromAST( | ||
fragment.selectionSet.selections, | ||
resolveInfo, | ||
tree, | ||
options, | ||
fragmentType, | ||
`${depth} ` | ||
); | ||
} | ||
} else if (kind === "InlineFragment" && options.deep) { | ||
const fragment = val; | ||
let fragmentType = parentType; | ||
if (fragment.typeCondition) { | ||
fragmentType = getType(resolveInfo, fragment.typeCondition); | ||
} | ||
debug( | ||
"%s[%d] Inline fragment (parent = '%s', type = '%s')", | ||
depth, | ||
instance, | ||
parentType, | ||
fragmentType | ||
); | ||
if (fragmentType) { | ||
fieldTreeFromAST( | ||
fragment.selectionSet.selections, | ||
resolveInfo, | ||
tree, | ||
options, | ||
fragmentType, | ||
`${depth} ` | ||
); | ||
} | ||
} else if (isReserved) { | ||
debug( | ||
"%s[%d] IGNORING because field '%s' is reserved", | ||
depth, | ||
instance, | ||
name | ||
); | ||
} else { | ||
debug( | ||
"%s[%d] IGNORING because kind '%s' not understood", | ||
depth, | ||
instance, | ||
kind | ||
); | ||
} | ||
// Ref: https://github.com/postgraphql/postgraphql/pull/342/files#diff-d6702ec9fed755c88b9d70b430fda4d8R148 | ||
return tree; | ||
}, initTree); | ||
} | ||
function firstKey(obj) { | ||
for (const key in obj) { | ||
return key; | ||
} | ||
} | ||
function getType(resolveInfo, typeCondition) { | ||
const { schema } = resolveInfo; | ||
const { kind, name } = typeCondition; | ||
if (kind === "NamedType") { | ||
const typeName = name.value; | ||
return schema.getType(typeName); | ||
} | ||
} | ||
function simplifyParsedResolveInfoFragmentWithType( | ||
parsedResolveInfoFragment, | ||
Type | ||
) { | ||
const { fieldsByTypeName } = parsedResolveInfoFragment; | ||
const fields = {}; | ||
const StrippedType = getNamedType(Type); | ||
Object.assign(fields, fieldsByTypeName[StrippedType.name]); | ||
if (StrippedType.getInterfaces) { | ||
// GraphQL ensures that the subfields cannot clash, so it's safe to simply overwrite them | ||
for (const Interface of StrippedType.getInterfaces()) { | ||
Object.assign(fields, fieldsByTypeName[Interface.name]); | ||
} | ||
} | ||
return Object.assign({}, parsedResolveInfoFragment, { | ||
fields, | ||
}); | ||
} | ||
parseFields.getAlias = getAlias; | ||
module.exports = parseFields; | ||
module.exports.simplifyParsedResolveInfoFragmentWithType = simplifyParsedResolveInfoFragmentWithType; | ||
module.exports.simplify = simplifyParsedResolveInfoFragmentWithType; |
{ | ||
"name": "graphql-parse-resolve-info", | ||
"version": "0.0.1-alpha2.0", | ||
"version": "0.0.1-alpha3.0", | ||
"description": "Parse GraphQLResolveInfo (the 4th argument of resolve) into a simple tree", | ||
"main": "index.js", | ||
"scripts": { | ||
"test": "jest ." | ||
"test": "jest .", | ||
"prepublish": "babel --out-dir lib src" | ||
}, | ||
@@ -27,4 +28,11 @@ "repository": { | ||
"peerDependencies": { | ||
"babel-polyfill": ">=6 <7", | ||
"graphql": ">=0.10 <1" | ||
}, | ||
"devDependencies": { | ||
"jest": "20.0.4" | ||
}, | ||
"jest": { | ||
"testRegex": "__tests__/.*\\.test\\.js$" | ||
} | ||
} |
321
README.md
@@ -14,10 +14,42 @@ graphql-parse-resolve-info | ||
Usage: requested subfields | ||
-------------------------- | ||
API | ||
--- | ||
To get the tree of subfields of the current field that are being requested: | ||
### `parseResolveInfo(resolveInfo)` | ||
Alias: `parse` | ||
Gets the tree of subfields of the current field that is being requested, | ||
returning the following properties (recursively): | ||
- `name`: the name of the GraphQL field | ||
- `alias`: the alias this GraphQL field has been requested as, or if no alias was specified then the `name` | ||
- `args`: the arguments this field was called with; at the root level this | ||
will be equivalent to the `args` that the `resolve(data, args, context, | ||
resolveInfo) {}` method receives, at deeper levels this allows you to get the | ||
`args` for the nested fields without waiting for their resolvers to be called. | ||
- `fieldsByTypeName`: an object keyed by GraphQL object type names, where the | ||
values are another object keyed by the aliases of the fields requested with | ||
values of the same format as the root level (i.e. `{alias, name, args, | ||
fieldsByTypeName}`); see below for an example | ||
Note that because GraphQL supports interfaces a resolver may return items of | ||
different types. For this reason, we key the fields by the GraphQL type name of | ||
the various fragments that were requested into the `fieldsByTypeName` field. | ||
Once you know which specific type the result is going to be, you can then use | ||
this type (and its interfaces) to determine which sub-fields were requested - | ||
we provide a `simplifyParsedResolveInfoFragmentWithType` helper to aid you with | ||
this. In many cases you will know what type the result will be (because it can | ||
only be one type) so you will probably use this helper heavily. | ||
Example usage: | ||
```js | ||
const parseResolveInfo = require('graphql-parse-resolve-info'); | ||
// or import parseResolveInfo from 'graphql-parse-resolve-info'; | ||
const { | ||
parseResolveInfo, | ||
simplifyParsedResolveInfoFragmentWithType | ||
} = require('graphql-parse-resolve-info'); | ||
// or import { parseResolveInfo, simplifyParsedResolveInfoFragmentWithType } from 'graphql-parse-resolve-info'; | ||
@@ -32,4 +64,6 @@ new GraphQLObjectType({ | ||
const parsedResolveInfoFragment = parseResolveInfo(resolveInfo); | ||
const { fields } = | ||
parseResolveInfo.simplifyParsedResolveInfoFragmentWithType(parsedResolveInfoFragment, ComplexType); | ||
const { fields } = simplifyParsedResolveInfoFragmentWithType( | ||
parsedResolveInfoFragment, | ||
ComplexType | ||
); | ||
console.dir(fields); | ||
@@ -43,17 +77,22 @@ ... | ||
(Note that because GraphQL supports interfaces and hence a resolver may return | ||
items of different types we key the fields by the GraphQL type name of the | ||
various fragments that were requested. Once you know what type the result was, | ||
you can then use this type (and its interfaces) to determine which sub-fields | ||
were requested. It's quite commont to know that your result will be of a single | ||
type, so we provide a helper that will simplify this for you by passing it the | ||
expected type.) | ||
### `simplifyParsedResolveInfoFragmentWithType(parsedResolveInfoFragment, ReturnType)` | ||
Usage: alias | ||
------------ | ||
Alias: `simplify` | ||
To get the alias of the current field being resolved (defaults to the field name if no alias was specified): | ||
Given an object of the form returned by `parseResolveInfo(...)` (which can be | ||
the root-level instance, or it could be one of the nested subfields) and a | ||
GraphQL type this method will return an object of the form above, with an | ||
additional field `fields` which only contains the fields compatible with the | ||
specified `ReturnType`. | ||
Or, in other words, this simplifies the `fieldsByTypeName` to an object of only | ||
the fields compatible with `ReturnType`. | ||
Example usage: | ||
```js | ||
const parseResolveInfo = require('graphql-parse-resolve-info'); | ||
const { | ||
parseResolveInfo, | ||
simplifyParsedResolveInfoFragmentWithType | ||
} = require('graphql-parse-resolve-info'); | ||
@@ -65,5 +104,37 @@ new GraphQLObjectType({ | ||
foo: { | ||
type: new GraphQLNonNull(ComplexType), | ||
resolve(data, args, context, resolveInfo) { | ||
const parsedResolveInfoFragment = parseResolveInfo(resolveInfo); | ||
const { fields } = simplifyParsedResolveInfoFragmentWithType( | ||
parsedResolveInfoFragment, | ||
ComplexType | ||
); | ||
... | ||
} | ||
} | ||
} | ||
}); | ||
``` | ||
### `getAliasFromResolveInfo(resolveInfo)` | ||
Alias: `getAlias` | ||
Returns the alias of the field being requested (or, if no alias was specified, | ||
then the name of the field). | ||
Example: | ||
```js | ||
const { getAliasFromResolveInfo } = require('graphql-parse-resolve-info'); | ||
new GraphQLObjectType({ | ||
name: ... | ||
fields: { | ||
... | ||
foo: { | ||
type: new GraphQLNonNull(GraphQLString), | ||
resolve(data, args, context, resolveInfo) { | ||
const alias = parseResolveInfo(resolveInfo, { aliasOnly: true }); | ||
const alias = getAliasFromResolveInfo(resolveInfo); | ||
return alias; | ||
@@ -76,2 +147,214 @@ } | ||
Example | ||
------- | ||
For the following GraphQL query: | ||
```graphql | ||
{ | ||
allPosts { | ||
edges { | ||
cursor | ||
node { | ||
...PostDetails | ||
author: personByAuthorId { | ||
firstPost { | ||
...PostDetails | ||
} | ||
friends { | ||
nodes { | ||
...PersonDetails | ||
} | ||
totalCount | ||
pageInfo { | ||
startCursor | ||
} | ||
} | ||
} | ||
} | ||
} | ||
} | ||
} | ||
fragment PersonDetails on Person { | ||
id | ||
name | ||
firstName | ||
} | ||
fragment PostDetails on Post { | ||
id | ||
headline | ||
headlineTrimmed | ||
author: personByAuthorId { | ||
...PersonDetails | ||
} | ||
} | ||
``` | ||
The following resolver in the `allPosts` field: | ||
```js | ||
const Query = new GraphQLObjectType({ | ||
name: 'Query', | ||
fields: { | ||
allPosts: { | ||
type: new GraphQLNonNull(PostsConnection), | ||
resolve(parent, args, context, resolveInfo) { | ||
const parsedResolveInfoFragment = parseResolveInfo( | ||
resolveInfo | ||
); | ||
const simplifiedFragment = simplifyParsedResolveInfoFragmentWithType( | ||
parsedResolveInfoFragment, | ||
resolveInfo.returnType | ||
); | ||
// ... | ||
}, | ||
} | ||
// ... | ||
} | ||
}); | ||
``` | ||
has `parsedResolveInfoFragment`: | ||
```js | ||
{ alias: 'allPosts', | ||
name: 'allPosts', | ||
args: {}, | ||
fieldsByTypeName: | ||
{ PostsConnection: | ||
{ edges: | ||
{ alias: 'edges', | ||
name: 'edges', | ||
args: {}, | ||
fieldsByTypeName: | ||
{ PostsEdge: | ||
{ cursor: | ||
{ alias: 'cursor', | ||
name: 'cursor', | ||
args: {}, | ||
fieldsByTypeName: {} }, | ||
node: | ||
{ alias: 'node', | ||
name: 'node', | ||
args: {}, | ||
fieldsByTypeName: | ||
{ Post: | ||
{ id: { alias: 'id', name: 'id', args: {}, fieldsByTypeName: {} }, | ||
headline: | ||
{ alias: 'headline', | ||
name: 'headline', | ||
args: {}, | ||
fieldsByTypeName: {} }, | ||
headlineTrimmed: | ||
{ alias: 'headlineTrimmed', | ||
name: 'headlineTrimmed', | ||
args: {}, | ||
fieldsByTypeName: {} }, | ||
author: | ||
{ alias: 'author', | ||
name: 'personByAuthorId', | ||
args: {}, | ||
fieldsByTypeName: | ||
{ Person: | ||
{ id: { alias: 'id', name: 'id', args: {}, fieldsByTypeName: {} }, | ||
name: { alias: 'name', name: 'name', args: {}, fieldsByTypeName: {} }, | ||
firstName: | ||
{ alias: 'firstName', | ||
name: 'firstName', | ||
args: {}, | ||
fieldsByTypeName: {} }, | ||
firstPost: | ||
{ alias: 'firstPost', | ||
name: 'firstPost', | ||
args: {}, | ||
fieldsByTypeName: | ||
{ Post: | ||
{ id: { alias: 'id', name: 'id', args: {}, fieldsByTypeName: {} }, | ||
headline: | ||
{ alias: 'headline', | ||
name: 'headline', | ||
args: {}, | ||
fieldsByTypeName: {} }, | ||
headlineTrimmed: | ||
{ alias: 'headlineTrimmed', | ||
name: 'headlineTrimmed', | ||
args: {}, | ||
fieldsByTypeName: {} }, | ||
author: | ||
{ alias: 'author', | ||
name: 'personByAuthorId', | ||
args: {}, | ||
fieldsByTypeName: | ||
{ Person: | ||
{ id: { alias: 'id', name: 'id', args: {}, fieldsByTypeName: {} }, | ||
name: { alias: 'name', name: 'name', args: {}, fieldsByTypeName: {} }, | ||
firstName: | ||
{ alias: 'firstName', | ||
name: 'firstName', | ||
args: {}, | ||
fieldsByTypeName: {} } } } } } } }, | ||
friends: | ||
{ alias: 'friends', | ||
name: 'friends', | ||
args: {}, | ||
fieldsByTypeName: | ||
{ PeopleConnection: | ||
{ nodes: | ||
{ alias: 'nodes', | ||
name: 'nodes', | ||
args: {}, | ||
fieldsByTypeName: | ||
{ Person: | ||
{ id: { alias: 'id', name: 'id', args: {}, fieldsByTypeName: {} }, | ||
name: { alias: 'name', name: 'name', args: {}, fieldsByTypeName: {} }, | ||
firstName: | ||
{ alias: 'firstName', | ||
name: 'firstName', | ||
args: {}, | ||
fieldsByTypeName: {} } } } }, | ||
totalCount: | ||
{ alias: 'totalCount', | ||
name: 'totalCount', | ||
args: {}, | ||
fieldsByTypeName: {} }, | ||
pageInfo: | ||
{ alias: 'pageInfo', | ||
name: 'pageInfo', | ||
args: {}, | ||
fieldsByTypeName: | ||
{ PageInfo: | ||
{ startCursor: | ||
{ alias: 'startCursor', | ||
name: 'startCursor', | ||
args: {}, | ||
fieldsByTypeName: {} } } } } } } } } } } } } } } } } } }, | ||
``` | ||
and the simplified `simplifiedFragment` is the same as | ||
`parsedResolveInfoFragment`, but with the additional root-level property | ||
`fields` which compresses the root-level property `fieldsByTypeName` to a | ||
single-level object containing only the fields compatible with | ||
`resolveInfo.returnType` (in this case: only `edges`): | ||
```js | ||
{ alias: 'allPosts', | ||
name: 'allPosts', | ||
args: {}, | ||
fieldsByTypeName: | ||
...as before... | ||
fields: | ||
{ edges: | ||
{ alias: 'edges', | ||
name: 'edges', | ||
args: {}, | ||
fieldsByTypeName: | ||
...as before... | ||
``` | ||
Thanks | ||
@@ -78,0 +361,0 @@ ------ |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
28278
6
400
359
2
1
1