Preoptimize queries and subscriptions documents

This commit is contained in:
Chi Vinh Le
2017-12-01 14:10:38 +01:00
parent 214de045fb
commit fd83fed7bb
4 changed files with 247 additions and 1 deletions
@@ -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];
};
}
+5 -1
View File
@@ -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
+1
View File
@@ -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",
+4
View File
@@ -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"