Created
August 22, 2018 15:41
-
-
Save prevostc/31fde404cc3896ff0bedab544e7e5169 to your computer and use it in GitHub Desktop.
Detect Graphql Selected Fields and subfields, account for aliases and fragments
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { | |
FieldNode, | |
FragmentSpreadNode, | |
GraphQLResolveInfo, | |
ResponsePath, | |
SelectionSetNode, | |
} from "graphql" | |
import { flatMap } from "lodash" | |
function instanceOfFieldNode(object: any): object is FieldNode { | |
return "kind" in object && object.kind === "Field" | |
} | |
function instanceOfFragmentSpreadNode( | |
object: any, | |
): object is FragmentSpreadNode { | |
return "kind" in object && object.kind === "FragmentSpread" | |
} | |
/** | |
* Resolve a field name and account for aliases | |
* Ex: | |
* query { q1: me { ... } } => "q1" | |
* query { me { ... } } => "me" | |
*/ | |
const getQueriedName = (field: FieldNode) => | |
field.alias ? field.alias.value : field.name.value | |
/** | |
* Retrieve the query path | |
* For a query like below, inside the createdNeeds resolver, the path will be [createdNeeds, privateData, me] | |
* me { | |
* privateData { | |
* createdNeeds { | |
* | |
* } | |
* } | |
* } | |
* @param path the info path | |
* @param agg | |
*/ | |
const getPath = (path: ResponsePath, agg: string[] = []): string[] => { | |
if (typeof path.key !== "string") { | |
throw new Error(`Unhandled non string key in graphql path: "${path.key}"`) | |
} | |
const newAgg = agg.concat([path.key]) | |
if (path.prev) { | |
return getPath(path.prev, newAgg) | |
} else { | |
return newAgg | |
} | |
} | |
/** | |
* Retrieve the selection set field list | |
* If some fragments are present, resolve those | |
* @param info | |
* @param selectionSet Current selection set, used to recurse | |
*/ | |
const getFieldNodesWithResolvedFragments = ( | |
info: GraphQLResolveInfo, | |
selectionSet: SelectionSetNode, | |
) => { | |
// extract fragment field nodes | |
const fragmentNodes = selectionSet.selections.filter( | |
instanceOfFragmentSpreadNode, | |
) | |
const fragmendFields = flatMap(fragmentNodes, (f: FragmentSpreadNode) => | |
info.fragments[f.name.value].selectionSet.selections.filter( | |
instanceOfFieldNode, | |
), | |
) | |
// extract "normal" nodes | |
const fields = selectionSet.selections.filter(instanceOfFieldNode) | |
return fields.concat(fragmendFields) | |
} | |
/** | |
* Retrieve the selection set of the current resolver | |
* Even if it is nested inside another resolver | |
* @param info the info object | |
* @param selectionSet Current selection set, used to recurse | |
* @param path Path extracted from info with getPath | |
*/ | |
const getSelectionSetFromPath = ( | |
info: GraphQLResolveInfo, | |
selectionSet: SelectionSetNode, | |
path: string[], | |
): SelectionSetNode => { | |
const fieldNodes = getFieldNodesWithResolvedFragments(info, selectionSet) | |
const fieldNode = fieldNodes.find(s => getQueriedName(s) === path[0]) | |
if (!fieldNode) { | |
throw new Error(`Could not find FieldNode named ${path[0]}`) | |
} | |
if (path.length <= 1) { | |
return fieldNode.selectionSet | |
} else { | |
return getSelectionSetFromPath( | |
info, | |
fieldNode.selectionSet, | |
path.slice(1, path.length), | |
) | |
} | |
} | |
/** | |
* Find out if a field below the current selection has been queries | |
* Usefull to load relations uphill | |
* | |
* ex: if childFieldPath = ['publicData', category] and the query is and the resolver is createdNeeds | |
* me { | |
* privateData { | |
* createdNeeds { | |
* uuid | |
* publicData { | |
* category | |
* } | |
* } | |
* } | |
* } | |
* Then this function returns true | |
* | |
* @param info the graphql info object | |
* @param childFieldName the path at which you may find the field | |
*/ | |
export const hasSelectedField = ( | |
info: GraphQLResolveInfo, | |
childFieldPath: string[], | |
) => { | |
const selectionSet = getSelectionSetFromPath( | |
info, | |
info.operation.selectionSet, | |
getPath(info.path) | |
.reverse() | |
.concat(childFieldPath.slice(0, childFieldPath.length - 1)), | |
) | |
const fieldNodes = getFieldNodesWithResolvedFragments(info, selectionSet) | |
const fieldExists = fieldNodes.some( | |
s => getQueriedName(s) === childFieldPath[childFieldPath.length - 1], | |
) | |
return fieldExists | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { gql } from "apollo-server-express" | |
import { GraphQLResolveInfo } from "graphql" | |
import { | |
FunnelQueryArgs, | |
} from "../../.codegen/typings/types" | |
import { Funnel } from "../../db/models/funnel" | |
import { Context } from "../context" | |
import { hasSelectedField } from "../graphql" | |
const schema = gql` | |
type Funnel { | |
uuid: UuidType! | |
keywords: [Keyword] | |
} | |
type Query { | |
"Fetch funneldata" | |
funnel(uuid: UuidType): Funnel | |
} | |
` | |
const resolvers = { | |
Query: { | |
funnel: async ( | |
_: never, | |
args: FunnelQueryArgs, | |
context: Context, | |
info: GraphQLResolveInfo, | |
): Promise<Funnel | null> => { | |
const relations: string[] = [] | |
if (hasSelectedField(info, ["keywords"])) { | |
relations.push("keywords") | |
} | |
if (hasSelectedField(info, ["keywords","domain"])) { | |
relations.push("keywords.domain") | |
} | |
const funnel = await context.container.getRepository(Funnel).findOne({ | |
relations, | |
where: { uuid: args.uuid }, | |
}) | |
if (!funnel) { | |
return null | |
} | |
return funnel | |
}, | |
}, | |
} | |
export { schema, resolvers } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
I found it very useful, thank you very much