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

Commit

Permalink
feat(stitching): precompile fragment replacements
Browse files Browse the repository at this point in the history
  • Loading branch information
yaacovCR committed Nov 19, 2019
1 parent 0a5aa6b commit 53e3662
Show file tree
Hide file tree
Showing 9 changed files with 380 additions and 108 deletions.
6 changes: 6 additions & 0 deletions src/Interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
GraphQLInputObjectType,
GraphQLInterfaceType,
GraphQLObjectType,
InlineFragmentNode,
} from 'graphql';

import { SchemaDirectiveVisitor } from './utils/SchemaDirectiveVisitor';
Expand Down Expand Up @@ -142,10 +143,15 @@ export type MergeInfo = {
field: string;
fragment: string;
}>;
replacementFragments: ReplacementFragmentMapping,
mergedTypes: Record<string, Array<SubschemaConfig>>,
delegateToSchema<TContext>(options: IDelegateToSchemaOptions<TContext>): any;
};

export type ReplacementFragmentMapping = {
[typeName: string]: { [fieldName: string]: InlineFragmentNode };
};

export type IFieldResolver<TSource, TContext, TArgs = Record<string, any>> = (
source: TSource,
args: TArgs,
Expand Down
6 changes: 3 additions & 3 deletions src/stitching/delegateToSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ import AddTypenameToAbstract from '../transforms/AddTypenameToAbstract';
import CheckResultAndHandleErrors from '../transforms/CheckResultAndHandleErrors';
import mapAsyncIterator from './mapAsyncIterator';
import ExpandAbstractTypes from '../transforms/ExpandAbstractTypes';
import ReplaceFieldWithFragment from '../transforms/ReplaceFieldWithFragment';
import AddReplacementFragments from '../transforms/AddReplacementFragments';

import { ApolloLink, execute as executeLink } from 'apollo-link';
import linkToFetcher from './linkToFetcher';
Expand Down Expand Up @@ -103,9 +103,9 @@ async function delegateToSchemaImplementation({
new ExpandAbstractTypes(info.schema, targetSchema),
];

if (info.mergeInfo && info.mergeInfo.fragments) {
if (info.mergeInfo) {
transforms.push(
new ReplaceFieldWithFragment(targetSchema, info.mergeInfo.fragments)
new AddReplacementFragments(targetSchema, info.mergeInfo.replacementFragments)
);
}

Expand Down
49 changes: 45 additions & 4 deletions src/stitching/mergeSchemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import {
DocumentNode,
GraphQLNamedType,
GraphQLObjectType,
GraphQLResolveInfo,
GraphQLScalarType,
GraphQLSchema,
extendSchema,
Expand All @@ -21,6 +20,8 @@ import {
SchemaLikeObject,
IResolvers,
SubschemaConfig,
ReplacementFragmentMapping,
IGraphQLToolsResolveInfo,
} from '../Interfaces';
import {
extractExtensionDefinitions,
Expand All @@ -31,8 +32,8 @@ import typeFromAST from './typeFromAST';
import {
Transform,
ExpandAbstractTypes,
ReplaceFieldWithFragment,
wrapSchema,
AddReplacementFragments,
} from '../transforms';
import {
SchemaDirectiveVisitor,
Expand All @@ -41,6 +42,8 @@ import {
healTypes,
forEachField,
mergeDeep,
parseFragmentToInlineFragment,
concatInlineFragments,
} from '../utils';

type MergeTypeCandidate = {
Expand Down Expand Up @@ -262,6 +265,8 @@ export default function mergeSchemas({
});
});

mergeInfo.replacementFragments = parseReplacementFragments(fragments);

addResolveFunctionsToSchema({
schema: mergedSchema,
resolvers: resolvers as IResolvers,
Expand Down Expand Up @@ -311,7 +316,7 @@ function createMergeInfo(
fieldName: string,
args: { [key: string]: any },
context: { [key: string]: any },
info: GraphQLResolveInfo,
info: IGraphQLToolsResolveInfo,
transforms?: Array<Transform>,
) {
console.warn(
Expand All @@ -320,7 +325,7 @@ function createMergeInfo(
);
const schema = guessSchemaByRootField(allSchemas, operation, fieldName);
const expandTransforms = new ExpandAbstractTypes(info.schema, schema);
const fragmentTransform = new ReplaceFieldWithFragment(schema, fragments);
const fragmentTransform = new AddReplacementFragments(schema, info.mergeInfo.replacementFragments);
return delegateToSchema({
schema,
operation,
Expand All @@ -343,10 +348,46 @@ function createMergeInfo(
});
},
fragments,
replacementFragments: undefined,
mergedTypes,
};
}

function parseReplacementFragments(
fragments: Array<{
field: string;
fragment: string;
}>
): ReplacementFragmentMapping {
const mapping = {};
for (const { field, fragment } of fragments) {
const parsedFragment = parseFragmentToInlineFragment(fragment);
const actualTypeName = parsedFragment.typeCondition.name.value;
mapping[actualTypeName] = mapping[actualTypeName] || {};

if (mapping[actualTypeName][field]) {
mapping[actualTypeName][field].push(parsedFragment);
} else {
mapping[actualTypeName][field] = [parsedFragment];
}
}

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

return replacementFragments;
}

function guessSchemaByRootField(
schemas: Array<GraphQLSchema>,
operation: 'query' | 'mutation' | 'subscription',
Expand Down
110 changes: 110 additions & 0 deletions src/test/testTransforms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,12 @@ import {
ReplaceFieldWithFragment,
FilterToSchema,
TransformQuery,
AddReplacementFragments,
} from '../transforms';
import {
concatInlineFragments,
parseFragmentToInlineFragment
} from '../utils';

describe('transforms', () => {
describe('base transform function', () => {
Expand Down Expand Up @@ -1118,3 +1123,108 @@ describe('transforms', () => {
});
});
});

describe('replaces field with processed fragment node', () => {
let data: any;
let schema: GraphQLSchema;
let subSchema: GraphQLSchema;
before(() => {
data = {
u1: {
id: 'u1',
name: 'joh',
surname: 'gats',
},
};

subSchema = makeExecutableSchema({
typeDefs: `
type User {
id: ID!
name: String!
surname: String!
}
type Query {
userById(id: ID!): User
}
`,
resolvers: {
Query: {
userById(parent, { id }) {
return data[id];
},
},
},
});

schema = makeExecutableSchema({
typeDefs: `
type User {
id: ID!
name: String!
surname: String!
fullname: String!
}
type Query {
userById(id: ID!): User
}
`,
resolvers: {
Query: {
userById(parent, { id }, context, info) {
return delegateToSchema({
schema: subSchema,
operation: 'query',
fieldName: 'userById',
args: { id },
context,
info,
transforms: [
new AddReplacementFragments(subSchema, {
User: {
fullname: concatInlineFragments(
'User',
[
parseFragmentToInlineFragment(`fragment UserName on User { name }`),
parseFragmentToInlineFragment(`fragment UserSurname on User { surname }`),
],
),
}
}),
],
});
},
},
User: {
fullname(parent, args, context, info) {
return `${parent.name} ${parent.surname}`;
},
},
},
});
});
it('should work', async () => {
const result = await graphql(
schema,
`
query {
userById(id: "u1") {
id
fullname
}
}
`,
);

expect(result).to.deep.equal({
data: {
userById: {
id: 'u1',
fullname: 'joh gats',
},
},
});
});
});
78 changes: 78 additions & 0 deletions src/transforms/AddReplacementFragments.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import {
DocumentNode,
GraphQLSchema,
GraphQLType,
Kind,
SelectionSetNode,
TypeInfo,
visit,
visitWithTypeInfo,
} from 'graphql';
import { Request, ReplacementFragmentMapping } from '../Interfaces';
import { Transform } from './transforms';

export default class AddReplacementFragments implements Transform {
private targetSchema: GraphQLSchema;
private fragments: ReplacementFragmentMapping;

constructor(
targetSchema: GraphQLSchema,
fragments: ReplacementFragmentMapping,
) {
this.targetSchema = targetSchema;
this.fragments = fragments;
}

public transformRequest(originalRequest: Request): Request {
const document = replaceFieldsWithFragments(
this.targetSchema,
originalRequest.document,
this.fragments,
);
return {
...originalRequest,
document,
};
}
}

function replaceFieldsWithFragments(
targetSchema: GraphQLSchema,
document: DocumentNode,
fragments: ReplacementFragmentMapping,
): 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 (fragments[parentTypeName]) {
node.selections.forEach(selection => {
if (selection.kind === Kind.FIELD) {
const name = selection.name.value;
const fragment = fragments[parentTypeName][name];
if (fragment) {
selections = selections.concat(fragment);
}
}
});
}

if (selections !== node.selections) {
return {
...node,
selections,
};
}
}
},
}),
);
}

0 comments on commit 53e3662

Please sign in to comment.