diff --git a/.changeset/spotty-bobcats-check.md b/.changeset/spotty-bobcats-check.md new file mode 100644 index 00000000000..1077a17cb65 --- /dev/null +++ b/.changeset/spotty-bobcats-check.md @@ -0,0 +1,5 @@ +--- +'@graphql-tools/wrap': minor +--- + +New transform: AddArgumentsAsVariables diff --git a/packages/wrap/src/transforms/AddArgumentsAsVariables.ts b/packages/wrap/src/transforms/AddArgumentsAsVariables.ts new file mode 100644 index 00000000000..924ccc3597b --- /dev/null +++ b/packages/wrap/src/transforms/AddArgumentsAsVariables.ts @@ -0,0 +1,165 @@ +import { + ArgumentNode, + DocumentNode, + FragmentDefinitionNode, + GraphQLArgument, + GraphQLField, + GraphQLObjectType, + GraphQLSchema, + Kind, + OperationDefinitionNode, + SelectionNode, + SelectionSetNode, + VariableDefinitionNode, +} from 'graphql'; + +import { Transform, DelegationContext } from '@graphql-tools/delegate'; + +import { + ExecutionRequest, + serializeInputValue, + updateArgument, + createVariableNameGenerator, +} from '@graphql-tools/utils'; + +interface AddArgumentsAsVariablesTransformationContext extends Record {} + +export default class AddArgumentsAsVariables> + implements Transform +{ + private readonly args: Record; + + constructor(args: Record) { + this.args = Object.entries(args).reduce( + (prev, [key, val]) => ({ + ...prev, + [key]: val, + }), + {} + ); + } + + public transformRequest( + originalRequest: ExecutionRequest, + delegationContext: DelegationContext, + _transformationContext: AddArgumentsAsVariablesTransformationContext + ): ExecutionRequest { + const { document, variables } = addVariablesToRootField(delegationContext.targetSchema, originalRequest, this.args); + + return { + ...originalRequest, + document, + variables, + }; + } +} + +function addVariablesToRootField( + targetSchema: GraphQLSchema, + originalRequest: ExecutionRequest, + args: Record +): { + document: DocumentNode; + variables: Record; +} { + const document = originalRequest.document; + const variableValues = originalRequest.variables ?? {}; + + const operations: Array = document.definitions.filter( + def => def.kind === Kind.OPERATION_DEFINITION + ) as Array; + const fragments: Array = document.definitions.filter( + def => def.kind === Kind.FRAGMENT_DEFINITION + ) as Array; + + const newOperations = operations.map((operation: OperationDefinitionNode) => { + const variableDefinitionMap: Record = (operation.variableDefinitions ?? []).reduce( + (prev, def) => ({ + ...prev, + [def.variable.name.value]: def, + }), + {} + ); + + let type: GraphQLObjectType | null | undefined; + if (operation.operation === 'subscription') { + type = targetSchema.getSubscriptionType(); + } else if (operation.operation === 'mutation') { + type = targetSchema.getMutationType(); + } else { + type = targetSchema.getQueryType(); + } + const newSelectionSet: Array = []; + + operation.selectionSet.selections.forEach((selection: SelectionNode) => { + if (selection.kind === Kind.FIELD) { + const argumentNodes = selection.arguments ?? []; + const argumentNodeMap: Record = argumentNodes.reduce( + (prev, argument) => ({ + ...prev, + [argument.name.value]: argument, + }), + {} + ); + + const targetField = type?.getFields()[selection.name.value]; + + // excludes __typename + if (targetField != null) { + updateArguments(targetField, argumentNodeMap, variableDefinitionMap, variableValues, args); + } + + newSelectionSet.push({ + ...selection, + arguments: Object.keys(argumentNodeMap).map(argName => argumentNodeMap[argName]), + }); + } else { + newSelectionSet.push(selection); + } + }); + + return { + ...operation, + variableDefinitions: Object.keys(variableDefinitionMap).map(varName => variableDefinitionMap[varName]), + selectionSet: { + kind: Kind.SELECTION_SET, + selections: newSelectionSet, + } as SelectionSetNode, + }; + }); + + return { + document: { + ...document, + definitions: [...newOperations, ...fragments], + }, + variables: variableValues, + }; +} + +function updateArguments( + targetField: GraphQLField, + argumentNodeMap: Record, + variableDefinitionMap: Record, + variableValues: Record, + newArgs: Record +): void { + const generateVariableName = createVariableNameGenerator(variableDefinitionMap); + + targetField.args.forEach((argument: GraphQLArgument) => { + const argName = argument.name; + const argType = argument.type; + + if (argName in newArgs) { + updateArgument( + argumentNodeMap, + variableDefinitionMap, + variableValues, + argName, + generateVariableName(argName), + argType, + serializeInputValue(argType, newArgs[argName]) + ); + } + }); +} diff --git a/packages/wrap/src/transforms/index.ts b/packages/wrap/src/transforms/index.ts index 2e5605b690a..3a6fcb29aae 100644 --- a/packages/wrap/src/transforms/index.ts +++ b/packages/wrap/src/transforms/index.ts @@ -1,3 +1,4 @@ +export { default as AddArgumentsAsVariables } from './AddArgumentsAsVariables'; export { default as RenameTypes } from './RenameTypes'; export { default as FilterTypes } from './FilterTypes'; export { default as RenameRootTypes } from './RenameRootTypes'; diff --git a/packages/wrap/tests/transformAddArgumentsAsVariables.test.ts b/packages/wrap/tests/transformAddArgumentsAsVariables.test.ts new file mode 100644 index 00000000000..a3713a07920 --- /dev/null +++ b/packages/wrap/tests/transformAddArgumentsAsVariables.test.ts @@ -0,0 +1,65 @@ +import { wrapSchema, AddArgumentsAsVariables } from '@graphql-tools/wrap'; +import { makeExecutableSchema } from '@graphql-tools/schema'; +import { parse, execute } from 'graphql'; +import { assertSome } from '@graphql-tools/utils'; + +describe('AddArgumentsAsVariables', () => { + const schema = makeExecutableSchema({ + typeDefs: /* GraphQL */ ` + input InputObject { + field1: String + field2: String + } + + type OutputObject { + field1: String + field2: String + field3: String + field4: String + } + + type Query { + test(argument: InputObject, otherArgument: String, thirdArgument: String): OutputObject + } + `, + resolvers: { + Query: { + test: (_root, args) => { + return { ...args.argument, field3: args.otherArgument, field4: args.thirdArgument }; + }, + }, + }, + }); + + const transformedSchema = wrapSchema({ + schema, + transforms: [ + new AddArgumentsAsVariables({ argument: { field1: 'field1', field2: 'field2' }, thirdArgument: 'field4' }), + ], + }); + + test('adds provided arguments as variables', async () => { + const query = /* GraphQL */ ` + { + test(otherArgument: "field3") { + field1 + field2 + field3 + field4 + } + } + `; + + const result = await execute({ + schema: transformedSchema, + document: parse(query), + }); + assertSome(result.data); + expect(result.errors).toBeUndefined(); + const dataTest: any = result.data['test']; + expect(dataTest.field1).toBe('field1'); + expect(dataTest.field2).toBe('field2'); + expect(dataTest.field3).toBe('field3'); + expect(dataTest.field4).toBe('field4'); + }); +});