Skip to content
Closed
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
2 changes: 2 additions & 0 deletions .cspell/cspell-dict.txt
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,8 @@ fiels
finalised
follwoing
fooz
fragmentified
fragmentify
Fragmentization
fufilled
Gmsuh
Expand Down
177 changes: 175 additions & 2 deletions internals-js/src/__tests__/operations.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ import {
} from '../../dist/definitions';
import { buildSchema } from '../../dist/buildSchema';
import { FederationBlueprint } from '../../dist/federation';
import { FragmentRestrictionAtType, MutableSelectionSet, NamedFragmentDefinition, Operation, operationFromDocument, parseOperation } from '../../dist/operations';
import { fragmentify, FragmentRestrictionAtType, MutableSelectionSet, NamedFragmentDefinition, Operation, operationFromDocument, operationToDocument, parseOperation } from '../../dist/operations';
import './matchers';
import { DocumentNode, FieldNode, GraphQLError, Kind, OperationDefinitionNode, OperationTypeNode, parse, SelectionNode, SelectionSetNode, validate } from 'graphql';
import { DocumentNode, FieldNode, GraphQLError, Kind, OperationDefinitionNode, OperationTypeNode, parse, print, SelectionNode, SelectionSetNode, validate } from 'graphql';
import { assert } from '../utils';
import gql from 'graphql-tag';

Expand Down Expand Up @@ -2422,6 +2422,179 @@ describe('fragments optimization', () => {
}
`);
});

describe('fragmentify', () => {
test('inline fragments to fragment definitions', () => {
const schema = parseSchema(`
type Query {
t: I
}

interface I {
b: Int
u: U
}

type T1 implements I {
a: Int
b: Int
u: U
}

type T2 implements I {
x: String
y: String
b: Int
u: U
}

union U = T1 | T2
`);
const operation = parseOperation(schema, `
{
t {
... on T1 {
a
b
}
... on T2 {
x
y
}
b
u {
... on I {
b
}
... on T1 {
a
b
}
... on T2 {
x
y
}
}
}
}
`);
const fragmentifiedDocument = fragmentify(operationToDocument(operation), 1);
const fragmentified = print(fragmentifiedDocument);
expect(fragmentified).toMatchString(`
{
t {
...T1FragmentV1
...T2FragmentV1
b
u {
...IFragmentV1
...T1FragmentV1
...T2FragmentV1
}
}
}

fragment T1FragmentV1 on T1 {
a
b
}

fragment T2FragmentV1 on T2 {
x
y
}

fragment IFragmentV1 on I {
b
}
`);
});

test(`wont create fragment definition for less than 2 selections`, () => {
const schema = parseSchema(`
type Query {
t: I
}

interface I {
b: Int
u: U
}

type T1 implements I {
a: Int
b: Int
u: U
}

type T2 implements I {
x: String
y: String
b: Int
u: U
}

union U = T1 | T2
`);
const operation = parseOperation(schema, `
{
t {
... on T1 {
a
b
}
... on T2 {
x
y
}
b
u {
... on I {
b
}
... on T1 {
a
b
}
... on T2 {
x
y
}
}
}
}
`);

const fragmentifiedDocument = fragmentify(operationToDocument(operation), 2);
const fragmentified = print(fragmentifiedDocument);
expect(fragmentified).toMatchString(`
{
t {
...T1FragmentV1
...T2FragmentV1
b
u {
... on I {
b
}
...T1FragmentV1
...T2FragmentV1
}
}
}

fragment T1FragmentV1 on T1 {
a
b
}

fragment T2FragmentV1 on T2 {
x
y
}
`);
});
});
});

describe('validations', () => {
Expand Down
118 changes: 118 additions & 0 deletions internals-js/src/operations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@ import {
SelectionSetNode,
OperationTypeNode,
NameNode,
visit,
} from "graphql";
import { ObjMap } from "graphql/jsutils/ObjMap";
import {
baseType,
Directive,
Expand Down Expand Up @@ -3898,3 +3900,119 @@ export function operationToDocument(operation: Operation): DocumentNode {
definitions: [operationAST as DefinitionNode].concat(fragmentASTs),
};
}

interface CountedFragmentDefinitionNode {
total: number;
fragments: ObjMap<FragmentDefinitionNode>
}

interface InlineFragmentToFragmentSpreadResult {
fragmentSpread: FragmentSpreadNode;
fragmentDefinition: FragmentDefinitionNode;
isNewFragmentDefinition: boolean;
}

export function fragmentify(
document: DocumentNode,
minSelectionsForFragment: number = 2
): DocumentNode {
const newFragmentDefinitions: ObjMap<CountedFragmentDefinitionNode> = {};

function getInlineFragmentSelections(
inlineFragment: InlineFragmentNode
): string[] {
const selectionSetsToVisit: SelectionSetNode[] = [inlineFragment.selectionSet];
const names: string[] = [];
let selectionSet: SelectionSetNode | undefined;
while((selectionSet = selectionSetsToVisit.pop())) {
for (const selection of selectionSet.selections) {
if ((selection.kind === Kind.FIELD || selection.kind === Kind.INLINE_FRAGMENT) && selection.selectionSet) {
selectionSetsToVisit.push(selection.selectionSet);
} else if (selection.kind === Kind.FIELD) {
names.push(selection.alias?.value ?? selection.name.value);
} else if (selection.kind === Kind.FRAGMENT_SPREAD) {
names.push(selection.name.value);
}
}
}
return names;
}

function inlineFragmentToFragmentSpread(
inlineFragment: InlineFragmentNode
): InlineFragmentToFragmentSpreadResult | undefined {
// we are only interested in inline fragments with TypeCondition
// example: ...on User { }
// Constrain #1
// inline fragments without TypeCondition: ...friendFields
// are going to be skipped as we would need the schema to know which type this fragment is applied to.
if (!inlineFragment.typeCondition) return undefined;
// Constrain #2
// we are not going to attempt to create fragment definitions for inline fragments with directives
// ... @include(if: $shouldInclude)
if (inlineFragment.directives && inlineFragment.directives.length > 0) return undefined;
// Constrain #3
// we are not going to attempt to create as fragment definition for inline fragments with less than [minSelectionsForFragment]
const inlineFragmentSelections = getInlineFragmentSelections(inlineFragment);
if (minSelectionsForFragment > inlineFragmentSelections.length) return undefined;

const inlineFragmentTypeName = inlineFragment.typeCondition.name.value;
const fragmentName = `${inlineFragmentTypeName}Fragment`;
const fragmentIdentifier = inlineFragmentSelections.join(',');

if (!newFragmentDefinitions[fragmentName]) {
newFragmentDefinitions[fragmentName] = {
total: 0,
fragments: {}
};
}

let isNewFragmentDefinition = false;
if (!newFragmentDefinitions[fragmentName].fragments[fragmentIdentifier]) {
const fragmentNameWithVersion = `${fragmentName}V${++newFragmentDefinitions[fragmentName].total}`;
const fragmentDefinitionNode: FragmentDefinitionNode = {
kind: Kind.FRAGMENT_DEFINITION,
name: { kind: Kind.NAME, value: fragmentNameWithVersion },
typeCondition: {
kind: Kind.NAMED_TYPE,
name: { kind: Kind.NAME, value: inlineFragmentTypeName }
},
selectionSet: inlineFragment.selectionSet,
};
newFragmentDefinitions[fragmentName].fragments[fragmentIdentifier] = fragmentDefinitionNode;
isNewFragmentDefinition = true;
}

return {
fragmentSpread: {
kind: Kind.FRAGMENT_SPREAD,
name: {
kind: Kind.NAME,
value: newFragmentDefinitions[fragmentName].fragments[fragmentIdentifier].name.value
}
},
fragmentDefinition: newFragmentDefinitions[fragmentName].fragments[fragmentIdentifier],
isNewFragmentDefinition
}
}

const newDocument = visit(document, {
SelectionSet: (selectionSet: SelectionSetNode): SelectionSetNode => {
for (let i = 0; i < selectionSet.selections.length; i++) {
if (selectionSet.selections[i].kind === Kind.INLINE_FRAGMENT) {
const inlineFragmentNode = selectionSet.selections[i] as InlineFragmentNode
const result = inlineFragmentToFragmentSpread(inlineFragmentNode);
if (result) {
(selectionSet.selections[i] as any) = result.fragmentSpread;
if (result.isNewFragmentDefinition) {
(document.definitions as any).push(result.fragmentDefinition);
}
}
}
}
return selectionSet;
}
});

return newDocument;
}