From 7a3e68da098736b726c6d2fa74c1dcfe26d10d64 Mon Sep 17 00:00:00 2001 From: Cris Naranjo Date: Fri, 4 Apr 2025 19:27:41 +0200 Subject: [PATCH 1/3] fix(executor): input object coercion rejects arrays --- src/execution/__tests__/executor-test.ts | 116 +++++++++++++++++++++++ src/utilities/coerceInputValue.ts | 2 +- 2 files changed, 117 insertions(+), 1 deletion(-) diff --git a/src/execution/__tests__/executor-test.ts b/src/execution/__tests__/executor-test.ts index c758d3e426..2af805f43f 100644 --- a/src/execution/__tests__/executor-test.ts +++ b/src/execution/__tests__/executor-test.ts @@ -11,6 +11,7 @@ import { Kind } from '../../language/kinds'; import { parse } from '../../language/parser'; import { + GraphQLInputObjectType, GraphQLInterfaceType, GraphQLList, GraphQLNonNull, @@ -1321,3 +1322,118 @@ describe('Execute: Handles basic execution tasks', () => { expect(possibleTypes).to.deep.equal([fooObject]); }); }); + +describe('Execute: Input coercion', () => { + it('should reject an array directly when input is an object', () => { + const schema = new GraphQLSchema({ + query: new GraphQLObjectType({ + name: 'Query', + fields: { + dummy: { type: GraphQLString }, + }, + }), + mutation: new GraphQLObjectType({ + name: 'Mutation', + fields: { + updateUser: { + type: GraphQLString, + args: { + data: { + type: new GraphQLInputObjectType({ + name: 'User', + fields: { + email: { type: new GraphQLNonNull(GraphQLString) }, + }, + }), + }, + }, + }, + }, + }), + }); + + const document = parse(` + mutation ($data: User) { + updateUser(data: $data) + } + `); + + const result = executeSync({ + schema, + document, + variableValues: { + data: [ + { email: 'test@email.com' }, + { email: 'test1@email.com' }, + { email: 'test2@email.com' }, + ], + }, + }); + + expect(result.errors).to.have.lengthOf(1); + expect( + result.errors && result.errors.length > 0 + ? result.errors[result.errors.length - 1].message + : undefined, + ).to.contain('Expected type "User" to be an object.'); + }); + + it('should not process an array when variable is an object', () => { + const schema = new GraphQLSchema({ + query: new GraphQLObjectType({ + name: 'Query', + fields: { + dummy: { type: GraphQLString }, + }, + }), + mutation: new GraphQLObjectType({ + name: 'Mutation', + fields: { + updateUser: { + type: GraphQLString, + args: { + data: { + type: new GraphQLInputObjectType({ + name: 'User', + fields: { + metadata: { + type: new GraphQLInputObjectType({ + name: 'Metadata', + fields: { + location: { type: GraphQLString }, + }, + }), + }, + }, + }), + }, + }, + }, + }, + }), + }); + + const document = parse(` + mutation ($data: User) { + updateUser(data: $data) + } + `); + + const result = executeSync({ + schema, + document, + variableValues: { + data: { + metadata: [{ location: 'USA' }, { location: 'USA' }], + }, + }, + }); + + expect(result.errors).to.have.lengthOf(1); + expect( + result.errors && result.errors.length > 0 + ? result.errors[result.errors.length - 1].message + : undefined, + ).to.contain('Expected type "Metadata" to be an object.'); + }); +}); diff --git a/src/utilities/coerceInputValue.ts b/src/utilities/coerceInputValue.ts index eeab0614f8..5263e925ee 100644 --- a/src/utilities/coerceInputValue.ts +++ b/src/utilities/coerceInputValue.ts @@ -86,7 +86,7 @@ function coerceInputValueImpl( } if (isInputObjectType(type)) { - if (!isObjectLike(inputValue)) { + if (!isObjectLike(inputValue) || Array.isArray(inputValue)) { onError( pathToArray(path), inputValue, From 5113acdf955a9633a352ea782f2b0cfa7872960c Mon Sep 17 00:00:00 2001 From: Cris Naranjo Date: Fri, 4 Apr 2025 20:26:33 +0200 Subject: [PATCH 2/3] test(executor): add tests for input coercion rejecting arrays --- src/execution/__tests__/executor-test.ts | 116 ------------------ .../__tests__/coerceInputValue-test.ts | 35 +++++- 2 files changed, 34 insertions(+), 117 deletions(-) diff --git a/src/execution/__tests__/executor-test.ts b/src/execution/__tests__/executor-test.ts index 2af805f43f..c758d3e426 100644 --- a/src/execution/__tests__/executor-test.ts +++ b/src/execution/__tests__/executor-test.ts @@ -11,7 +11,6 @@ import { Kind } from '../../language/kinds'; import { parse } from '../../language/parser'; import { - GraphQLInputObjectType, GraphQLInterfaceType, GraphQLList, GraphQLNonNull, @@ -1322,118 +1321,3 @@ describe('Execute: Handles basic execution tasks', () => { expect(possibleTypes).to.deep.equal([fooObject]); }); }); - -describe('Execute: Input coercion', () => { - it('should reject an array directly when input is an object', () => { - const schema = new GraphQLSchema({ - query: new GraphQLObjectType({ - name: 'Query', - fields: { - dummy: { type: GraphQLString }, - }, - }), - mutation: new GraphQLObjectType({ - name: 'Mutation', - fields: { - updateUser: { - type: GraphQLString, - args: { - data: { - type: new GraphQLInputObjectType({ - name: 'User', - fields: { - email: { type: new GraphQLNonNull(GraphQLString) }, - }, - }), - }, - }, - }, - }, - }), - }); - - const document = parse(` - mutation ($data: User) { - updateUser(data: $data) - } - `); - - const result = executeSync({ - schema, - document, - variableValues: { - data: [ - { email: 'test@email.com' }, - { email: 'test1@email.com' }, - { email: 'test2@email.com' }, - ], - }, - }); - - expect(result.errors).to.have.lengthOf(1); - expect( - result.errors && result.errors.length > 0 - ? result.errors[result.errors.length - 1].message - : undefined, - ).to.contain('Expected type "User" to be an object.'); - }); - - it('should not process an array when variable is an object', () => { - const schema = new GraphQLSchema({ - query: new GraphQLObjectType({ - name: 'Query', - fields: { - dummy: { type: GraphQLString }, - }, - }), - mutation: new GraphQLObjectType({ - name: 'Mutation', - fields: { - updateUser: { - type: GraphQLString, - args: { - data: { - type: new GraphQLInputObjectType({ - name: 'User', - fields: { - metadata: { - type: new GraphQLInputObjectType({ - name: 'Metadata', - fields: { - location: { type: GraphQLString }, - }, - }), - }, - }, - }), - }, - }, - }, - }, - }), - }); - - const document = parse(` - mutation ($data: User) { - updateUser(data: $data) - } - `); - - const result = executeSync({ - schema, - document, - variableValues: { - data: { - metadata: [{ location: 'USA' }, { location: 'USA' }], - }, - }, - }); - - expect(result.errors).to.have.lengthOf(1); - expect( - result.errors && result.errors.length > 0 - ? result.errors[result.errors.length - 1].message - : undefined, - ).to.contain('Expected type "Metadata" to be an object.'); - }); -}); diff --git a/src/utilities/__tests__/coerceInputValue-test.ts b/src/utilities/__tests__/coerceInputValue-test.ts index 3692ce26b7..6940b298f2 100644 --- a/src/utilities/__tests__/coerceInputValue-test.ts +++ b/src/utilities/__tests__/coerceInputValue-test.ts @@ -182,12 +182,20 @@ describe('coerceInputValue', () => { }); }); - describe('for GraphQLInputObject', () => { + describe.only('for GraphQLInputObject', () => { + const DeepObject = new GraphQLInputObjectType({ + name: 'DeepObject', + fields: { + foo: { type: new GraphQLNonNull(GraphQLInt) }, + bar: { type: GraphQLInt }, + }, + }); const TestInputObject = new GraphQLInputObjectType({ name: 'TestInputObject', fields: { foo: { type: new GraphQLNonNull(GraphQLInt) }, bar: { type: GraphQLInt }, + deepObject: { type: DeepObject }, }, }); @@ -271,6 +279,31 @@ describe('coerceInputValue', () => { }, ]); }); + + it('returns an error for an array type', () => { + const result = coerceValue([{ foo: 1 }, { bar: 1 }], TestInputObject); + expectErrors(result).to.deep.equal([ + { + error: 'Expected type "TestInputObject" to be an object.', + path: [], + value: [{ foo: 1 }, { bar: 1 }], + }, + ]); + }); + + it('returns an error for an array type on a nested field', () => { + const result = coerceValue( + { foo: 1, deepObject: [1, 2, 3] }, + TestInputObject, + ); + expectErrors(result).to.deep.equal([ + { + error: 'Expected type "DeepObject" to be an object.', + path: ['deepObject'], + value: [1, 2, 3], + }, + ]); + }); }); describe('for GraphQLInputObject that isOneOf', () => { From 86aab2196a8305ba22aa75ccb75e4d6e6024fdb6 Mon Sep 17 00:00:00 2001 From: Cris Naranjo Date: Fri, 4 Apr 2025 20:27:30 +0200 Subject: [PATCH 3/3] test(coerceInputValue): remove exclusive focus from GraphQLInputObject tests --- src/utilities/__tests__/coerceInputValue-test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utilities/__tests__/coerceInputValue-test.ts b/src/utilities/__tests__/coerceInputValue-test.ts index 6940b298f2..03afca6a6b 100644 --- a/src/utilities/__tests__/coerceInputValue-test.ts +++ b/src/utilities/__tests__/coerceInputValue-test.ts @@ -182,7 +182,7 @@ describe('coerceInputValue', () => { }); }); - describe.only('for GraphQLInputObject', () => { + describe('for GraphQLInputObject', () => { const DeepObject = new GraphQLInputObjectType({ name: 'DeepObject', fields: {