Skip to content

Instantly share code, notes, and snippets.

@prevostc
Created August 22, 2018 15:41
Show Gist options
  • Save prevostc/31fde404cc3896ff0bedab544e7e5169 to your computer and use it in GitHub Desktop.
Save prevostc/31fde404cc3896ff0bedab544e7e5169 to your computer and use it in GitHub Desktop.
Detect Graphql Selected Fields and subfields, account for aliases and fragments
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
}
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 }
@davp00
Copy link

davp00 commented Mar 27, 2021

I found it very useful, thank you very much

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment