diff --git a/packages/links/package.json b/packages/links/package.json index e71e1690b37..c435fb3041e 100644 --- a/packages/links/package.json +++ b/packages/links/package.json @@ -23,6 +23,7 @@ "graphql-upload": "11.0.0" }, "dependencies": { + "@graphql-tools/utils": "6.0.3", "apollo-link": "1.2.14", "apollo-upload-client": "13.0.0", "form-data": "3.0.0", diff --git a/packages/utils/src/build-operation-for-field.ts b/packages/utils/src/build-operation-for-field.ts index fb157ad38af..f016611dd7b 100644 --- a/packages/utils/src/build-operation-for-field.ts +++ b/packages/utils/src/build-operation-for-field.ts @@ -51,6 +51,12 @@ export type Skip = string[]; export type Force = string[]; export type Ignore = string[]; +export type SelectedFields = + | { + [key: string]: SelectedFields; + } + | boolean; + export function buildOperationNodeForField({ schema, kind, @@ -60,6 +66,7 @@ export function buildOperationNodeForField({ depthLimit, circularReferenceDepth, argNames, + selectedFields = true, }: { schema: GraphQLSchema; kind: OperationTypeNode; @@ -69,6 +76,7 @@ export function buildOperationNodeForField({ depthLimit?: number; circularReferenceDepth?: number; argNames?: string[]; + selectedFields?: SelectedFields; }) { resetOperationVariables(); resetFieldMap(); @@ -82,6 +90,7 @@ export function buildOperationNodeForField({ depthLimit: depthLimit || Infinity, circularReferenceDepth: circularReferenceDepth || 1, argNames, + selectedFields, }); // attach variables @@ -102,6 +111,7 @@ function buildOperationAndCollectVariables({ depthLimit, circularReferenceDepth, argNames, + selectedFields, }: { schema: GraphQLSchema; fieldName: string; @@ -111,6 +121,7 @@ function buildOperationAndCollectVariables({ depthLimit: number; circularReferenceDepth: number; argNames?: string[]; + selectedFields: SelectedFields; }): OperationDefinitionNode { const typeMap: Record = { query: schema.getQueryType()!, @@ -154,6 +165,7 @@ function buildOperationAndCollectVariables({ schema, depth: 0, argNames, + selectedFields, }), ], }, @@ -173,6 +185,7 @@ function resolveSelectionSet({ schema, depth, argNames, + selectedFields, }: { parent: GraphQLNamedType; type: GraphQLNamedType; @@ -185,9 +198,10 @@ function resolveSelectionSet({ circularReferenceDepth: number; schema: GraphQLSchema; depth: number; + selectedFields: SelectedFields; argNames?: string[]; }): SelectionSetNode | void { - if (depth > depthLimit) { + if (typeof selectedFields === 'boolean' && depth > depthLimit) { return; } if (isUnionType(type)) { @@ -224,19 +238,11 @@ function resolveSelectionSet({ schema, depth, argNames, + selectedFields, }) as SelectionSetNode, }; }) - .filter(f => { - if (f) { - if ('selectionSet' in f) { - return f.selectionSet?.selections?.length; - } else { - return true; - } - } - return false; - }), + .filter(fragmentNode => fragmentNode?.selectionSet?.selections?.length > 0), }; } @@ -276,9 +282,11 @@ function resolveSelectionSet({ schema, depth, argNames, + selectedFields, }) as SelectionSetNode, }; - }), + }) + .filter(fragmentNode => fragmentNode?.selectionSet?.selections?.length > 0), }; } @@ -312,19 +320,23 @@ function resolveSelectionSet({ }); }) .map(fieldName => { - return resolveField({ - type: type, - field: fields[fieldName], - models, - path: [...path, fieldName], - ancestors, - ignore, - depthLimit, - circularReferenceDepth, - schema, - depth, - argNames, - }); + const selectedSubFields = typeof selectedFields === 'object' ? selectedFields[fieldName] : true; + if (selectedSubFields) { + return resolveField({ + type: type, + field: fields[fieldName], + models, + path: [...path, fieldName], + ancestors, + ignore, + depthLimit, + circularReferenceDepth, + schema, + depth, + argNames, + selectedFields: selectedSubFields, + }); + } }) .filter(f => { if (f) { @@ -398,6 +410,7 @@ function resolveField({ schema, depth, argNames, + selectedFields, }: { type: GraphQLObjectType; field: GraphQLField; @@ -410,6 +423,7 @@ function resolveField({ circularReferenceDepth: number; schema: GraphQLSchema; depth: number; + selectedFields: SelectedFields; argNames?: string[]; }): SelectionNode { const namedType = getNamedType(field.type); @@ -480,6 +494,7 @@ function resolveField({ schema, depth: depth + 1, argNames, + selectedFields, }) || undefined, arguments: args, }; diff --git a/packages/utils/tests/build-operation-node-for-field.spec.ts b/packages/utils/tests/build-operation-node-for-field.spec.ts index d56e8ab5cad..7422f0a0c3d 100644 --- a/packages/utils/tests/build-operation-node-for-field.spec.ts +++ b/packages/utils/tests/build-operation-node-for-field.spec.ts @@ -519,3 +519,44 @@ test('arguments', async () => { `) ); }); + +test('selectedFields', async () => { + + const document = buildOperationNodeForField({ + schema, + kind: 'query', + field: 'user', + selectedFields: { + favoriteFood: { + dough: true, + toppings: true, + asian: true, + }, + shelf: true, // Add all nested fields + } + })!; + + expect(clean(document)).toEqual( + clean(/* GraphQL */ ` + query userQuery($id: ID!) { + user(id: $id) { + favoriteFood { + ... on Pizza { + dough + toppings + } + ... on Salad { + ... on Coleslaw { + asian + } + } + } + shelf { + id + title + } + } + } + `) + ); +})