From fd83fed7bb068f5efa453ff4c1131ba0acbf43a3 Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Fri, 1 Dec 2017 14:10:38 +0100 Subject: [PATCH] Preoptimize queries and subscriptions documents --- .../coral-framework/graphql/reduceDocument.js | 237 ++++++++++++++++++ client/coral-framework/hocs/withQuery.js | 6 +- package.json | 1 + yarn.lock | 4 + 4 files changed, 247 insertions(+), 1 deletion(-) create mode 100644 client/coral-framework/graphql/reduceDocument.js diff --git a/client/coral-framework/graphql/reduceDocument.js b/client/coral-framework/graphql/reduceDocument.js new file mode 100644 index 000000000..d0a9cc30f --- /dev/null +++ b/client/coral-framework/graphql/reduceDocument.js @@ -0,0 +1,237 @@ +import { + getMainDefinition, + getFragmentDefinitions, + createFragmentMap, + shouldInclude, + getOperationDefinition, +} from 'apollo-utilities'; + +function getDefinitionName(definition) { + switch (definition.kind) { + case 'FragmentSpread': + return definition.name.value; + case 'Field': + return `Field_${definition.alias ? definition.alias.value : definition.name.value}`; + case 'InlineFragment': + return `InlineFragment_${definition.typeCondition.name.value}`; + default: + throw new Error(`unknown definition kind ${definition.kind}`); + } +} + +/** + * Merge selections of 2 definitions. + */ +export function mergeDefinitions(a, b) { + const name = getDefinitionName(a); + + if (!!a.selectionSet !== !!b.selectionSet) { + throw Error(`incompatible field definition for ${name}`); + } + + if (!a.selectionSet) { + return b; + } + + const selectionSet = mergeSelectionSets(a.selectionSet, b.selectionSet); + + return { + ...b, + selectionSet, + }; +} + +/** + * Merge selectionSets + */ +export function mergeSelectionSets(a, b) { + const selectionsMap = [...a.selections, ...b.selections].reduce((o, sel) => { + const selName = getDefinitionName(sel); + if (!(selName in o)) { + o[selName] = sel; + return o; + } + o[selName] = mergeDefinitions(o[selName], sel); + return o; + }, {}); + + const selections = Object.keys(selectionsMap).map((key) => selectionsMap[key]); + + return { + ...b, + selections, + }; +} + +/** + * Return selections with resolved named fragments and directives. + */ +function getTransformedSelections(definition, path, gqlType, execContext) { + const { + fragmentMap, + variables, + } = execContext; + + const selectionsMap = definition.selectionSet.selections.reduce((o, sel) => { + if (variables && !shouldInclude(sel, variables)) { + + // Skip this entirely + return o; + } + if (sel.kind !== 'FragmentSpread') { + const transformed = transformDefinition(sel, execContext, path, gqlType); + const name = getDefinitionName(sel); + + // Merge existing value. + if (name in o) { + o[name] = mergeDefinitions(o[name], transformed); + return o; + } + + o[name] = transformed; + return o; + } + + const fragment = fragmentMap[sel.name.value]; + + if (!fragment) { + throw new Error(`fragment ${fragment.name.value} does not exist`); + } + + const typeCondition = fragment.typeCondition.name.value; + + if (gqlType !== typeCondition) { + const node = { + ...fragment, + kind: 'InlineFragment', + }; + const transformed = transformDefinition(node, execContext, path, typeCondition); + const name = getDefinitionName(node); + + // Merge existing value. + if (name in o) { + o[name] = mergeDefinitions(o[name], transformed); + return o; + } + + o[name] = transformed; + return o; + } + + const fragmentSelections = getTransformedSelections(fragment, path, typeCondition, execContext); + fragmentSelections.forEach((s) => { + + if (variables && !shouldInclude(s, variables)) { + + // Skip this entirely + return; + } + + const selName = getDefinitionName(s); + if (!(selName in o)) { + o[selName] = s; + return; + } + + o[selName] = mergeDefinitions(o[selName], s); + }); + return o; + }, {}); + + const selections = Object.keys(selectionsMap).map((key) => selectionsMap[key]); + return selections; +} + +/** + * Resolve named fragments and directives in a definition. + */ +function transformDefinition(definition, execContext, path = '', type = null) { + if (!definition.selectionSet) { + return definition; + } + + const {typeGetter} = execContext; + + if (definition.kind === 'Field') { + const fieldName = definition.name.value; + path = `${path}.${fieldName}`; + + if (typeGetter) { + type = typeGetter(path); + } + } + + // InlineFragments + else if(!type && typeGetter) { + type = typeGetter(path); + } + + return { + ...definition, + selectionSet: { + ...definition.selectionSet, + selections: getTransformedSelections(definition, path, type, execContext), + }, + }; +} + +export default function reduceDocument(document, options = {}) { + const mainDefinition = getMainDefinition(document); + const fragments = getFragmentDefinitions(document); + const operationDefinition = getOperationDefinition(document); + const path = operationDefinition.operation; + + const execContext = { + fragmentMap: createFragmentMap(fragments), + keepFragments: [], + variables: options.variables, + typeGetter: options.typeGetter || (() => null), + }; + + return { + kind: 'Document', + definitions: [transformDefinition(mainDefinition, execContext, path)], + }; +} + +function getObjectType(fieldType) { + if (['NON_NULL', 'LIST'].indexOf(fieldType.kind) > -1) { + return getObjectType(fieldType.ofType); + } + return fieldType.name; +} + +function getFieldType(parentType, fieldName) { + const field = parentType.fields.find((f) => f.name === fieldName); + return getObjectType(field.type); +} + +export function createTypeGetter(introspectionData) { + const types = {}; + introspectionData.__schema.types.forEach((type) => types[type.name] = type); + + const result = { + 'query': introspectionData.__schema.queryType.name, + 'mutation': introspectionData.__schema.mutationType.name, + 'subscription': introspectionData.__schema.subscriptionType.name, + }; + + return (path) => { + if (result[path]) { + return result[path]; + } + let currentPath = ''; + const parts = path.split('.'); + for (let i = 0; i < parts.length; i++) { + const part = parts[i]; + const nextPath = currentPath ? `${currentPath}.${part}` : part; + if (nextPath in result) { + currentPath = nextPath; + continue; + } + result[nextPath] = getFieldType(types[result[currentPath]], part); + currentPath = nextPath; + } + return result[path]; + }; +} diff --git a/client/coral-framework/hocs/withQuery.js b/client/coral-framework/hocs/withQuery.js index eaf740ffb..dac2bb221 100644 --- a/client/coral-framework/hocs/withQuery.js +++ b/client/coral-framework/hocs/withQuery.js @@ -6,6 +6,10 @@ import hoistStatics from 'recompose/hoistStatics'; import {getOperationName} from 'apollo-client/queries/getFromAST'; import {addTypenameToDocument} from 'apollo-client/queries/queryTransform'; import throttle from 'lodash/throttle'; +import reduceDocument, {createTypeGetter} from '../graphql/reduceDocument'; +import introspectionData from '../graphql/introspection.json'; + +const typeGetter = createTypeGetter(introspectionData); const withSkipOnErrors = (reducer) => (prev, action, ...rest) => { if (action.type === 'APOLLO_MUTATION_RESULT' && getResponseErrors(action.result)) { @@ -68,7 +72,7 @@ export default (document, config = {}) => hoistStatics((WrappedComponent) => { let document = typeof documentOrCallback === 'function' ? documentOrCallback(this.props, this.context) : documentOrCallback; - document = this.graphqlRegistry.resolveFragments(document); + document = reduceDocument(this.graphqlRegistry.resolveFragments(document), {typeGetter}); // We also add typenames to the document which apollo would usually do, // but we also use the network interface in subscriptions directly diff --git a/package.json b/package.json index 7a71114a3..9cdc2a03d 100644 --- a/package.json +++ b/package.json @@ -60,6 +60,7 @@ "dependencies": { "accepts": "^1.3.4", "apollo-client": "^1.9.1", + "apollo-utilities": "^1.0.3", "app-module-path": "^2.2.0", "autoprefixer": "^6.5.2", "babel-cli": "6.26.0", diff --git a/yarn.lock b/yarn.lock index f393a3f99..6d31966e6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -259,6 +259,10 @@ apollo-link-core@^0.5.0: graphql-tag "^2.4.2" zen-observable-ts "^0.4.4" +apollo-utilities@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/apollo-utilities/-/apollo-utilities-1.0.3.tgz#bf435277609850dd442cf1d5c2e8bc6655eaa943" + app-module-path@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/app-module-path/-/app-module-path-2.2.0.tgz#641aa55dfb7d6a6f0a8141c4b9c0aa50b6c24dd5"