Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Multiple fragments support #895

Merged
merged 9 commits into from
Jul 31, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

* Loosens the apollo-link dependency [PR #765](https://github.com/apollographql/graphql-tools/pull/765)
* Use `getDescription` from `graphql-js` package [PR #672](https://github.com/apollographql/graphql-tools/pull/672)
* Add support for overlapping fragments in ReplaceFieldWithFragment. [#894](https://github.com/apollographql/graphql-tools/issues/894)

### v3.0.5

Expand Down
111 changes: 108 additions & 3 deletions src/test/testTransforms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
FilterTypes,
WrapQuery,
ExtractField,
ReplaceFieldWithFragment,
} from '../transforms';

describe('transforms', () => {
Expand Down Expand Up @@ -101,8 +102,8 @@ describe('transforms', () => {
}
}
properties(limit: 1) {
__typename
id
__typename
id
}
propertyById(id: "p1") {
... on Property_Property {
Expand Down Expand Up @@ -133,7 +134,7 @@ describe('transforms', () => {
{
__typename: 'Property_Property',
id: 'p1',
}
},
],
propertyById: {
id: 'p1',
Expand Down Expand Up @@ -478,4 +479,108 @@ describe('transforms', () => {
});
});
});

describe('replaces field with fragments', () => {
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 ReplaceFieldWithFragment(subSchema, [
{
field: `fullname`,
fragment: `fragment UserName on User { name }`,
},
{
field: `fullname`,
fragment: `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',
},
},
});
});
});
});
118 changes: 114 additions & 4 deletions src/transforms/ReplaceFieldWithFragment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
parse,
visit,
visitWithTypeInfo,
SelectionNode,
} from 'graphql';
import { Request } from '../Interfaces';
import { Transform } from './transforms';
Expand All @@ -31,7 +32,12 @@ export default class ReplaceFieldWithFragment implements Transform {
const parsedFragment = parseFragmentToInlineFragment(fragment);
const actualTypeName = parsedFragment.typeCondition.name.value;
this.mapping[actualTypeName] = this.mapping[actualTypeName] || {};
this.mapping[actualTypeName][field] = parsedFragment;

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

Expand All @@ -49,7 +55,7 @@ export default class ReplaceFieldWithFragment implements Transform {
}

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

function replaceFieldsWithFragments(
Expand All @@ -73,8 +79,12 @@ function replaceFieldsWithFragments(
node.selections.forEach(selection => {
if (selection.kind === Kind.FIELD) {
const name = selection.name.value;
const fragment = mapping[parentTypeName][name];
if (fragment) {
const fragments = mapping[parentTypeName][name];
if (fragments && fragments.length > 0) {
const fragment = concatInlineFragments(
parentTypeName,
fragments,
);
selections = selections.concat(fragment);
}
}
Expand Down Expand Up @@ -119,3 +129,103 @@ function parseFragmentToInlineFragment(

throw new Error('Could not parse fragment');
}

function concatInlineFragments(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, few issues here.

  • If fragments have conflicting aliases this will break
  • Maybe this should dedupe fields (also watch out for aliases in that case)

type: string,
fragments: InlineFragmentNode[],
): InlineFragmentNode {
const fragmentSelections: SelectionNode[] = fragments.reduce(
(selections, fragment) => {
return 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<{ [key: string]: SelectionNode }>(
(map, node) => {
switch (node.kind) {
case 'Field': {
if (node.alias) {
if (map.hasOwnProperty(node.alias.value)) {
return map;
} else {
return {
...map,
[node.alias.value]: node,
};
}
} else {
if (map.hasOwnProperty(node.name.value)) {
return map;
} else {
return {
...map,
[node.name.value]: node,
};
}
}
}
case 'FragmentSpread': {
if (map.hasOwnProperty(node.name.value)) {
return map;
} else {
return {
...map,
[node.name.value]: node,
};
}
}
case 'InlineFragment': {
if (map.__fragment) {
const fragment = map.__fragment as InlineFragmentNode;

return {
...map,
__fragment: concatInlineFragments(
fragment.typeCondition.name.value,
[fragment, node],
),
};
} else {
return {
...map,
__fragment: node,
};
}
}
default: {
return map;
}
}
},
{},
);

const selection = Object.keys(selectionMap).reduce(
(selectionList, node) => selectionList.concat(selectionMap[node]),
[],
);

return selection;
}