Skip to content

Commit

Permalink
Ignore unmerged fields (#6134)
Browse files Browse the repository at this point in the history
* Ignore unmerged fields

* Fix tests

* Add tests

* Changeset

* Typo
  • Loading branch information
ardatan committed May 7, 2024
1 parent 1630a35 commit a83da08
Show file tree
Hide file tree
Showing 9 changed files with 472 additions and 149 deletions.
49 changes: 49 additions & 0 deletions .changeset/olive-shirts-buy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
---
"@graphql-tools/delegate": patch
"@graphql-tools/stitch": patch
---

Ignore unmerged fields

Let's say you have a gateway schema like in the bottom, and `id` is added to the query, only if the `age` is requested;

```graphql
# This will be sent as-is
{
user {
name
}
}
```

But the following will be transformed;
```graphql
{
user {
name
age
}
}
```
Into
```graphql
{
user {
id
name
age
}
}


```graphql
type Query {
user: User
}

type User {
id: ID! # is the key for all services
name: String!
age: Int! # This comes from another service
}
```
138 changes: 138 additions & 0 deletions packages/delegate/src/extractUnavailableFields.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import {
FieldNode,
getNamedType,
GraphQLField,
GraphQLInterfaceType,
GraphQLNamedOutputType,
GraphQLNamedType,
GraphQLObjectType,
GraphQLSchema,
isAbstractType,
isInterfaceType,
isLeafType,
isObjectType,
isUnionType,
Kind,
SelectionNode,
SelectionSetNode,
} from 'graphql';
import { Maybe, memoize4 } from '@graphql-tools/utils';

export const extractUnavailableFieldsFromSelectionSet = memoize4(
function extractUnavailableFieldsFromSelectionSet(
schema: GraphQLSchema,
fieldType: GraphQLNamedOutputType,
fieldSelectionSet: SelectionSetNode,
shouldAdd: (
fieldType: GraphQLObjectType | GraphQLInterfaceType,
selection: FieldNode,
) => boolean,
) {
if (isLeafType(fieldType)) {
return [];
}
if (isUnionType(fieldType)) {
const unavailableSelections: SelectionNode[] = [];
for (const type of fieldType.getTypes()) {
// Exclude other inline fragments
const fieldSelectionExcluded: SelectionSetNode = {
...fieldSelectionSet,
selections: fieldSelectionSet.selections.filter(selection =>
selection.kind === Kind.INLINE_FRAGMENT
? selection.typeCondition
? selection.typeCondition.name.value === type.name
: false
: true,
),
};
unavailableSelections.push(
...extractUnavailableFieldsFromSelectionSet(
schema,
type,
fieldSelectionExcluded,
shouldAdd,
),
);
}
return unavailableSelections;
}
const subFields = fieldType.getFields();
const unavailableSelections: SelectionNode[] = [];
for (const selection of fieldSelectionSet.selections) {
if (selection.kind === Kind.FIELD) {
if (selection.name.value === '__typename') {
continue;
}
const fieldName = selection.name.value;
const selectionField = subFields[fieldName];
if (!selectionField) {
if (shouldAdd(fieldType, selection)) {
unavailableSelections.push(selection);
}
} else {
const unavailableSubFields = extractUnavailableFields(
schema,
selectionField,
selection,
shouldAdd,
);
if (unavailableSubFields.length) {
unavailableSelections.push({
...selection,
selectionSet: {
kind: Kind.SELECTION_SET,
selections: unavailableSubFields,
},
});
}
}
} else if (selection.kind === Kind.INLINE_FRAGMENT) {
const subFieldType: Maybe<GraphQLNamedType> = selection.typeCondition
? schema.getType(selection.typeCondition.name.value)
: fieldType;
if (
(isObjectType(subFieldType) || isInterfaceType(subFieldType)) &&
isAbstractType(fieldType) &&
schema.isSubType(fieldType, subFieldType)
) {
const unavailableFields = extractUnavailableFieldsFromSelectionSet(
schema,
subFieldType,
selection.selectionSet,
shouldAdd,
);
if (unavailableFields.length) {
unavailableSelections.push({
...selection,
selectionSet: {
kind: Kind.SELECTION_SET,
selections: unavailableFields,
},
});
}
} else {
unavailableSelections.push(selection);
}
}
}
return unavailableSelections;
},
);

export const extractUnavailableFields = memoize4(function extractUnavailableFields(
schema: GraphQLSchema,
field: GraphQLField<any, any>,
fieldNode: FieldNode,
shouldAdd: (fieldType: GraphQLObjectType | GraphQLInterfaceType, selection: FieldNode) => boolean,
) {
if (fieldNode.selectionSet) {
const fieldType = getNamedType(field.type);
return extractUnavailableFieldsFromSelectionSet(
schema,
fieldType,
fieldNode.selectionSet,
shouldAdd,
);
}
return [];
});
1 change: 1 addition & 0 deletions packages/delegate/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ export * from './mergeFields.js';
export * from './resolveExternalValue.js';
export * from './subschemaConfig.js';
export * from './types.js';
export * from './extractUnavailableFields.js';
40 changes: 27 additions & 13 deletions packages/delegate/src/prepareGatewayDocument.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
isAbstractType,
isInterfaceType,
isLeafType,
isObjectType,
Kind,
SelectionNode,
SelectionSetNode,
Expand All @@ -23,6 +24,7 @@ import {
implementsAbstractType,
memoize2,
} from '@graphql-tools/utils';
import { extractUnavailableFields } from './extractUnavailableFields.js';
import { getDocumentMetadata } from './getDocumentMetadata.js';
import { StitchingInfo } from './types.js';

Expand Down Expand Up @@ -101,6 +103,8 @@ export function prepareGatewayDocument(
);
}

const shouldAdd = () => true;

function visitSelectionSet(
node: SelectionSetNode,
fragmentReplacements: Record<string, Array<{ fragmentName: string; typeName: string }>>,
Expand All @@ -118,7 +122,6 @@ function visitSelectionSet(
): SelectionSetNode {
const newSelections = new Set<SelectionNode>();
const maybeType = typeInfo.getParentType();

if (maybeType != null) {
const parentType: GraphQLNamedType = getNamedType(maybeType);
const parentTypeName = parentType.name;
Expand Down Expand Up @@ -180,21 +183,32 @@ function visitSelectionSet(
}
} else {
const fieldName = selection.name.value;

const fieldNodes = fieldNodesByField[parentTypeName]?.[fieldName];
if (fieldNodes != null) {
for (const fieldNode of fieldNodes) {
newSelections.add(fieldNode);
let skipAddingDependencyNodes = false;
// TODO: Optimization to prevent extra fields to the subgraph
if (isObjectType(parentType) || isInterfaceType(parentType)) {
const fieldMap = parentType.getFields();
const field = fieldMap[fieldName];
if (field) {
const unavailableFields = extractUnavailableFields(schema, field, selection, shouldAdd);
skipAddingDependencyNodes = unavailableFields.length === 0;
}
}
if (!skipAddingDependencyNodes) {
const fieldNodes = fieldNodesByField[parentTypeName]?.[fieldName];
if (fieldNodes != null) {
for (const fieldNode of fieldNodes) {
newSelections.add(fieldNode);
}
}

const dynamicSelectionSets = dynamicSelectionSetsByField[parentTypeName]?.[fieldName];
if (dynamicSelectionSets != null) {
for (const selectionSetFn of dynamicSelectionSets) {
const selectionSet = selectionSetFn(selection);
if (selectionSet != null) {
for (const selection of selectionSet.selections) {
newSelections.add(selection);
const dynamicSelectionSets = dynamicSelectionSetsByField[parentTypeName]?.[fieldName];
if (dynamicSelectionSets != null) {
for (const selectionSetFn of dynamicSelectionSets) {
const selectionSet = selectionSetFn(selection);
if (selectionSet != null) {
for (const selection of selectionSet.selections) {
newSelections.add(selection);
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { getOperationAST, isObjectType, Kind, parse, print, SelectionSetNode } from 'graphql';
import { makeExecutableSchema } from '@graphql-tools/schema';
import { stripWhitespaces } from '../../merge/tests/utils';
import { extractUnavailableFields } from '../src/getFieldsNotInSubschema';
import { extractUnavailableFields } from '../src/extractUnavailableFields';

describe('extractUnavailableFields', () => {
it('should extract correct fields', () => {
Expand Down

0 comments on commit a83da08

Please sign in to comment.