diff --git a/src/validation/__tests__/UniqueInputFieldNamesRule-test.ts b/src/validation/__tests__/UniqueInputFieldNamesRule-test.ts index 33e4a2db01..8b7d57e344 100644 --- a/src/validation/__tests__/UniqueInputFieldNamesRule-test.ts +++ b/src/validation/__tests__/UniqueInputFieldNamesRule-test.ts @@ -53,6 +53,77 @@ describe('Validate: Unique input field names', () => { `); }); + it('allow and/or with duplicate fields in array', () => { + expectValid(` + { + field(arg: { and: [{ f: "value1" }, { f: "value2" }] }) + } + `); + expectValid(` + { + field(arg: { or: [{ f: { f1: "value1" } }, { f: { f1: "value2" } }] }) + } + `); + expectValid(` + { + field(arg: { or: [{ f: true }, { f1: {f: true} }] }) + } + `); + expectValid(` + { + field(arg: { + or: [ + { field: true }, + { + deep1: { + deep2: { + and: [{ field: false }, { field: true }] + } + } + } + { + deep1: { + field: true + } + } + ] + }) + } + `); + }); + + it('duplicate input object fields in objects of array', () => { + expectErrors(` + { + field(arg: { or: [{ f: "value1", f: "value2" }] }) + } + `).toDeepEqual([ + { + message: 'There can be only one input field named "f".', + locations: [ + { line: 3, column: 29 }, + { line: 3, column: 42 }, + ], + }, + ]); + }); + + it('nested input object fields in objects of array', () => { + expectErrors(` + { + field(arg: { or: [ { f2: "value1" }, { f1: { f2: "value2", f2: "value3" } } ] }) + } + `).toDeepEqual([ + { + message: 'There can be only one input field named "f2".', + locations: [ + { line: 3, column: 54 }, + { line: 3, column: 68 }, + ], + }, + ]); + }); + it('duplicate input object fields', () => { expectErrors(` { diff --git a/src/validation/rules/UniqueInputFieldNamesRule.ts b/src/validation/rules/UniqueInputFieldNamesRule.ts index c1916a73b3..0797020d7c 100644 --- a/src/validation/rules/UniqueInputFieldNamesRule.ts +++ b/src/validation/rules/UniqueInputFieldNamesRule.ts @@ -3,7 +3,7 @@ import type { ObjMap } from '../../jsutils/ObjMap'; import { GraphQLError } from '../../error/GraphQLError'; -import type { NameNode } from '../../language/ast'; +import type { NameNode, ObjectFieldNode } from '../../language/ast'; import type { ASTVisitor } from '../../language/visitor'; import type { ASTValidationContext } from '../ValidationContext'; @@ -22,6 +22,9 @@ export function UniqueInputFieldNamesRule( const knownNameStack: Array> = []; let knownNames: ObjMap = Object.create(null); + const knownNamesInListStack: Array>> = + []; + return { ObjectValue: { enter() { @@ -34,9 +37,48 @@ export function UniqueInputFieldNamesRule( knownNames = prevKnownNames; }, }, + ListValue: { + enter(node) { + const knownNamesInList: Array> = + Object.create([]); + node.values.forEach((valueNode) => { + if (valueNode.kind === 'ObjectValue') { + knownNamesInList.push(valueNode.fields); + } + }); + + knownNamesInListStack.push(knownNamesInList); + }, + leave() { + knownNamesInListStack.pop(); + }, + }, ObjectField(node) { const fieldName = node.name.value; + let isError = false; + if (knownNames[fieldName]) { + // get latest element in knownNamesInListStack + const knownNamesInList = + knownNamesInListStack[knownNamesInListStack.length - 1] || []; + + if (!knownNamesInList.length) { + isError = true; + } else { + for (const fields of knownNamesInList) { + const nestedFields = fields.filter( + (field) => field.name.value === fieldName, + ); + + // expecting only one field with the same name, if there is more than one, report error. if there is no field with the same name, mean it is in the nested object instead list value, report error. + if (!isError && nestedFields.length !== 1) { + isError = true; + } + } + } + } + + if (isError) { context.reportError( new GraphQLError( `There can be only one input field named "${fieldName}".`,