Skip to content
This repository has been archived by the owner on Apr 15, 2020. It is now read-only.

Commit

Permalink
feat(merging): merge additional types besides GraphQLObjectTypes
Browse files Browse the repository at this point in the history
  • Loading branch information
yaacovCR committed Feb 2, 2020
1 parent eea77bb commit 574ff85
Show file tree
Hide file tree
Showing 3 changed files with 351 additions and 260 deletions.
285 changes: 285 additions & 0 deletions src/stitching/mergeInfo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,285 @@
import {
GraphQLNamedType,
GraphQLObjectType,
GraphQLScalarType,
GraphQLSchema,
Kind,
SelectionNode,
SelectionSetNode,
} from 'graphql';
import {
IDelegateToSchemaOptions,
MergeInfo,
IResolversParameter,
isSubschemaConfig,
SubschemaConfig,
IGraphQLToolsResolveInfo,
MergedTypeInfo,
} from '../Interfaces';
import delegateToSchema from './delegateToSchema';
import {
Transform,
ExpandAbstractTypes,
AddReplacementFragments,
} from '../transforms';
import {
parseFragmentToInlineFragment,
concatInlineFragments,
typeContainsSelectionSet,
parseSelectionSet,
} from '../utils';
import { TypeMap } from 'graphql/type/schema';

type MergeTypeCandidate = {
type: GraphQLNamedType;
schema?: GraphQLSchema;
subschema?: GraphQLSchema | SubschemaConfig;
transformedSubschema?: GraphQLSchema;
};

export function createMergeInfo(
allSchemas: Array<GraphQLSchema>,
typeCandidates: { [name: string]: Array<MergeTypeCandidate> },
mergeTypes?: boolean | Array<string> |
((typeName: string, mergeTypeCandidates: Array<MergeTypeCandidate>) => boolean),
): MergeInfo {
return {
delegate(
operation: 'query' | 'mutation' | 'subscription',
fieldName: string,
args: { [key: string]: any },
context: { [key: string]: any },
info: IGraphQLToolsResolveInfo,
transforms?: Array<Transform>,
) {
console.warn(
'`mergeInfo.delegate` is deprecated. ' +
'Use `mergeInfo.delegateToSchema and pass explicit schema instances.',
);
const schema = guessSchemaByRootField(allSchemas, operation, fieldName);
const expandTransforms = new ExpandAbstractTypes(info.schema, schema);
const fragmentTransform = new AddReplacementFragments(schema, info.mergeInfo.replacementFragments);
return delegateToSchema({
schema,
operation,
fieldName,
args,
context,
info,
transforms: [
...(transforms || []),
expandTransforms,
fragmentTransform,
],
});
},

delegateToSchema(options: IDelegateToSchemaOptions) {
return delegateToSchema({
...options,
transforms: options.transforms
});
},
fragments: [],
replacementSelectionSets: undefined,
replacementFragments: undefined,
mergedTypes: createMergedTypes(typeCandidates, mergeTypes),
};
}

function createMergedTypes(
typeCandidates: { [name: string]: Array<MergeTypeCandidate> },
mergeTypes?: boolean | Array<string> |
((typeName: string, mergeTypeCandidates: Array<MergeTypeCandidate>) => boolean)
): Record<string, MergedTypeInfo> {
const mergedTypes: Record<string, MergedTypeInfo> = {};

Object.keys(typeCandidates).forEach(typeName => {
if (typeCandidates[typeName][0].type instanceof GraphQLObjectType) {
const mergedTypeCandidates = typeCandidates[typeName]
.filter(typeCandidate =>
typeCandidate.subschema &&
isSubschemaConfig(typeCandidate.subschema) &&
typeCandidate.subschema.merge &&
typeCandidate.subschema.merge[typeName]
);

if (
mergeTypes === true ||
(typeof mergeTypes === 'function') && mergeTypes(typeName, typeCandidates[typeName]) ||
Array.isArray(mergeTypes) && mergeTypes.includes(typeName) ||
mergedTypeCandidates.length
) {
const subschemas: Array<SubschemaConfig> = [];

let requiredSelections: Array<SelectionNode> = [parseSelectionSet(`{ __typename }`).selections[0]];
const fields = Object.create({});
const typeMaps: Map<SubschemaConfig, TypeMap> = new Map();
const selectionSets: Map<SubschemaConfig, SelectionSetNode> = new Map();

mergedTypeCandidates.forEach(typeCandidate => {
const subschemaConfig = typeCandidate.subschema as SubschemaConfig;
const transformedSubschema = typeCandidate.transformedSubschema;
typeMaps.set(subschemaConfig, transformedSubschema.getTypeMap());
const type = transformedSubschema.getType(typeName) as GraphQLObjectType;
const fieldMap = type.getFields();
Object.keys(fieldMap).forEach(fieldName => {
fields[fieldName] = fields[fieldName] || [];
fields[fieldName].push(subschemaConfig);
});

const mergedTypeConfig = subschemaConfig.merge[typeName];

if (mergedTypeConfig.selectionSet) {
const selectionSet = parseSelectionSet(mergedTypeConfig.selectionSet);
requiredSelections = requiredSelections.concat(selectionSet.selections);
selectionSets.set(subschemaConfig, selectionSet);
}

if (!mergedTypeConfig.resolve) {
mergedTypeConfig.resolve = (originalResult, context, info, subschema, selectionSet) => delegateToSchema({
schema: subschema,
operation: 'query',
fieldName: mergedTypeConfig.fieldName,
args: mergedTypeConfig.args(originalResult),
selectionSet,
context,
info,
skipTypeMerging: true,
});
}

subschemas.push(subschemaConfig);
});

mergedTypes[typeName] = {
subschemas,
typeMaps,
selectionSets,
containsSelectionSet: new Map(),
uniqueFields: Object.create({}),
nonUniqueFields: Object.create({}),
};

subschemas.forEach(subschema => {
const type = typeMaps.get(subschema)[typeName] as GraphQLObjectType<any, any>;
let subschemaMap = new Map();
subschemas.filter(s => s !== subschema).forEach(s => {
const selectionSet = selectionSets.get(s);
if (selectionSet && typeContainsSelectionSet(type, selectionSet)) {
subschemaMap.set(selectionSet, true);
}
});
mergedTypes[typeName].containsSelectionSet.set(subschema, subschemaMap);
});

Object.keys(fields).forEach(fieldName => {
const supportedBySubschemas = fields[fieldName];
if (supportedBySubschemas.length === 1) {
mergedTypes[typeName].uniqueFields[fieldName] = supportedBySubschemas[0];
} else {
mergedTypes[typeName].nonUniqueFields[fieldName] = supportedBySubschemas;
}
});

mergedTypes[typeName].selectionSet = {
kind: Kind.SELECTION_SET,
selections: requiredSelections,
};
}

}
});

return mergedTypes;
}

export function completeMergeInfo(
mergeInfo: MergeInfo,
resolvers: IResolversParameter,
): MergeInfo {
const replacementSelectionSets = Object.create(null);

Object.keys(resolvers).forEach(typeName => {
const type = resolvers[typeName];
if (type instanceof GraphQLScalarType) {
return;
}
Object.keys(type).forEach(fieldName => {
const field = type[fieldName];
if (field.selectionSet) {
const selectionSet = parseSelectionSet(field.selectionSet);
replacementSelectionSets[typeName] = replacementSelectionSets[typeName] || {};
replacementSelectionSets[typeName][fieldName] = replacementSelectionSets[typeName][fieldName] || {
kind: Kind.SELECTION_SET,
selections: [],
};
replacementSelectionSets[typeName][fieldName].selections =
replacementSelectionSets[typeName][fieldName].selections.concat(selectionSet.selections);
}
if (field.fragment) {
mergeInfo.fragments.push({
field: fieldName,
fragment: field.fragment,
});
}
});
});

const mapping = {};
mergeInfo.fragments.forEach(({ field, fragment }) => {
const parsedFragment = parseFragmentToInlineFragment(fragment);
const actualTypeName = parsedFragment.typeCondition.name.value;
mapping[actualTypeName] = mapping[actualTypeName] || {};
mapping[actualTypeName][field] = mapping[actualTypeName][field] || [];
mapping[actualTypeName][field].push(parsedFragment);
});

const replacementFragments = Object.create(null);
Object.keys(mapping).forEach(typeName => {
Object.keys(mapping[typeName]).forEach(field => {
replacementFragments[typeName] = mapping[typeName] || {};
replacementFragments[typeName][field] = concatInlineFragments(
typeName,
mapping[typeName][field],
);
});
});

mergeInfo.replacementSelectionSets = replacementSelectionSets;
mergeInfo.replacementFragments = replacementFragments;

return mergeInfo;
}

function operationToRootType(
operation: 'query' | 'mutation' | 'subscription',
schema: GraphQLSchema,
): GraphQLObjectType {
if (operation === 'subscription') {
return schema.getSubscriptionType();
} else if (operation === 'mutation') {
return schema.getMutationType();
} else {
return schema.getQueryType();
}
}

function guessSchemaByRootField(
schemas: Array<GraphQLSchema>,
operation: 'query' | 'mutation' | 'subscription',
fieldName: string,
): GraphQLSchema {
for (const schema of schemas) {
let rootObject = operationToRootType(operation, schema);
if (rootObject) {
const fields = rootObject.getFields();
if (fields[fieldName]) {
return schema;
}
}
}
throw new Error(
`Could not find subschema with field \`${operation}.${fieldName}\``,
);
}

0 comments on commit 574ff85

Please sign in to comment.