Created
September 6, 2018 16:00
-
-
Save pthrasher/6cc22bfc5e20cf5f30c53239f3d54eac to your computer and use it in GitHub Desktop.
This file contains hidden or 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
// @flow | |
/* eslint-disable no-use-before-define, consistent-return, no-prototype-builtins, no-underscore-dangle */ | |
// This was mostly ripped from: | |
// https://github.com/apollographql/graphql-tools/blob/master/src/transforms/ReplaceFieldWithFragment.ts | |
// It was easier to modify an existing transform to work than it was to | |
// write a new transform from scratch. | |
import { | |
DocumentNode, | |
GraphQLSchema, | |
GraphQLType, | |
InlineFragmentNode, | |
Kind, | |
SelectionSetNode, | |
TypeInfo, | |
OperationDefinitionNode, | |
parse, | |
visit, | |
visitWithTypeInfo, | |
SelectionNode, | |
} from 'graphql'; | |
import { Request, Transform } from 'graphql-tools'; | |
export default class AddInlineFragmentToType extends Transform { | |
targetSchema: GraphQLSchema; | |
mapping: FieldToFragmentMapping; | |
constructor( | |
targetSchema: GraphQLSchema, | |
fragments: Array<{ | |
typeName: string; | |
fragment: string; | |
}>, | |
) { | |
super(); | |
this.targetSchema = targetSchema; | |
this.mapping = {}; | |
fragments.forEach(({ fragment }) => { | |
const parsedFragment = parseFragmentToInlineFragment(fragment); | |
const actualTypeName = parsedFragment.typeCondition.name.value; | |
if (this.mapping[actualTypeName]) { | |
this.mapping[actualTypeName].push(parsedFragment); | |
} else { | |
this.mapping[actualTypeName] = [parsedFragment]; | |
} | |
}) | |
} | |
transformRequest(originalRequest: Request): Request { | |
const document = addFragmentsToTypes( | |
this.targetSchema, | |
originalRequest.document, | |
this.mapping, | |
); | |
return { | |
...originalRequest, | |
document, | |
}; | |
} | |
} | |
type FieldToFragmentMapping = { | |
[typeName: string]: InlineFragmentNode[] | |
}; | |
function addFragmentsToTypes( | |
targetSchema: GraphQLSchema, | |
document: DocumentNode, | |
mapping: FieldToFragmentMapping, | |
): DocumentNode { | |
const typeInfo = new TypeInfo(targetSchema); | |
return visit( | |
document, | |
visitWithTypeInfo(typeInfo, { | |
[Kind.SELECTION_SET]( | |
node: SelectionSetNode, | |
): SelectionSetNode | null | undefined { | |
const parentType: GraphQLType = typeInfo.getParentType(); | |
if (parentType) { | |
const parentTypeName = parentType.name; | |
let selections = node.selections; | |
if (mapping[parentTypeName]) { | |
const fragments = mapping[parentTypeName]; | |
if (fragments && fragments.length > 0) { | |
const fragment = concatInlineFragments( | |
parentTypeName, | |
fragments, | |
); | |
selections = selections.concat(fragment); | |
} | |
} | |
if (selections !== node.selections) { | |
return { | |
...node, | |
selections, | |
}; | |
} | |
} | |
}, | |
}), | |
); | |
} | |
function parseFragmentToInlineFragment( | |
definitions: string, | |
): InlineFragmentNode { | |
if (definitions.trim().startsWith('fragment')) { | |
const document = parse(definitions); | |
for (let i = 0; i < document.definitions.length; i++) { | |
const definition = document.definitions[i]; | |
if (definition.kind === Kind.FRAGMENT_DEFINITION) { | |
return { | |
kind: Kind.INLINE_FRAGMENT, | |
typeCondition: definition.typeCondition, | |
selectionSet: definition.selectionSet, | |
}; | |
} | |
} | |
} | |
const query: OperationDefinitionNode = parse(`{${definitions}}`) | |
.definitions[0]; | |
for (let i = 0; i < query.selectionSet.selections.length; i++) { | |
const selection = query.selectionSet.selections[i]; | |
if (selection.kind === Kind.INLINE_FRAGMENT) { | |
return selection; | |
} | |
} | |
throw new Error('Could not parse fragment'); | |
} | |
function concatInlineFragments( | |
type: string, | |
fragments: InlineFragmentNode[], | |
): InlineFragmentNode { | |
const fragmentSelections: SelectionNode[] = fragments.reduce( | |
(selections, fragment) => | |
selections.concat(fragment.selectionSet.selections), | |
[], | |
); | |
const deduplicatedFragmentSelection: SelectionNode[] = deduplicateSelection( | |
fragmentSelections, | |
); | |
return { | |
kind: Kind.INLINE_FRAGMENT, | |
typeCondition: { | |
kind: Kind.NAMED_TYPE, | |
name: { | |
kind: Kind.NAME, | |
value: type, | |
}, | |
}, | |
selectionSet: { | |
kind: Kind.SELECTION_SET, | |
selections: deduplicatedFragmentSelection, | |
}, | |
}; | |
} | |
function deduplicateSelection(nodes: SelectionNode[]): SelectionNode[] { | |
const selectionMap = nodes.reduce( | |
(map: { [key: string]: SelectionNode }, node: SelectionNode) => { | |
switch (node.kind) { | |
case 'Field': { | |
if (node.alias) { | |
if (map.hasOwnProperty(node.alias.value)) { | |
return map; | |
} | |
return { | |
...map, | |
[node.alias.value]: node, | |
}; | |
} else if (map.hasOwnProperty(node.name.value)) { | |
return map; | |
} | |
return { | |
...map, | |
[node.name.value]: node, | |
}; | |
} | |
case 'FragmentSpread': { | |
if (map.hasOwnProperty(node.name.value)) { | |
return map; | |
} | |
return { | |
...map, | |
[node.name.value]: node, | |
}; | |
} | |
case 'InlineFragment': { | |
if (map.__fragment) { | |
const fragment: InlineFragmentNode = map.__fragment; | |
return { | |
...map, | |
__fragment: concatInlineFragments( | |
fragment.typeCondition.name.value, | |
[fragment, node], | |
), | |
}; | |
} | |
return { | |
...map, | |
__fragment: node, | |
}; | |
} | |
default: { | |
return map; | |
} | |
} | |
}, | |
{}, | |
); | |
const selection = Object.keys(selectionMap).reduce( | |
(selectionList, node) => selectionList.concat(selectionMap[node]), | |
[], | |
); | |
return selection; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment