diff --git a/src/utilities/TypeInfo.ts b/src/utilities/TypeInfo.ts index e2c9683835..acc3c43926 100644 --- a/src/utilities/TypeInfo.ts +++ b/src/utilities/TypeInfo.ts @@ -116,6 +116,8 @@ export class TypeInfo { return this._inputTypeStack.at(-1); } + // Note: continues to expose the closest enclosing valid input type if + // traversal descends into syntax with no corresponding GraphQL input type. getParentInputType(): Maybe { return this._inputTypeStack.at(-2); } @@ -254,7 +256,7 @@ export class TypeInfo { const listType: unknown = getNullableType(this.getInputType()); const itemType: unknown = isListType(listType) ? listType.ofType - : listType; + : undefined; // List positions never have a default value. this._defaultValueStack.push(undefined); this._inputTypeStack.push(isInputType(itemType) ? itemType : undefined); diff --git a/src/utilities/__tests__/TypeInfo-test.ts b/src/utilities/__tests__/TypeInfo-test.ts index cc129287c3..1691dfff70 100644 --- a/src/utilities/__tests__/TypeInfo-test.ts +++ b/src/utilities/__tests__/TypeInfo-test.ts @@ -519,6 +519,121 @@ describe('visitWithTypeInfo', () => { ]); }); + it('supports traversals of object literals in custom scalar positions', () => { + const schema = buildSchema(` + scalar GeoPoint + `); + const ast = parseValue('{x: 4.0, y: 2.0}'); + const scalarType = schema.getType('GeoPoint'); + assert(scalarType != null); + + const typeInfo = new TypeInfo(schema, scalarType); + + const visited: Array = []; + visit( + ast, + visitWithTypeInfo(typeInfo, { + enter(node) { + const type = typeInfo.getInputType(); + const parentType = typeInfo.getParentInputType(); + visited.push([ + 'enter', + node.kind, + node.kind === 'Name' ? node.value : null, + String(type), + String(parentType), + ]); + }, + leave(node) { + const type = typeInfo.getInputType(); + const parentType = typeInfo.getParentInputType(); + visited.push([ + 'leave', + node.kind, + node.kind === 'Name' ? node.value : null, + String(type), + String(parentType), + ]); + }, + }), + ); + + expect(visited).to.deep.equal([ + // Everything within ObjectValue should have type: undefined since the + // contents of custom scalars are not part of the GraphQL type system. + // getParentInputType() continues to report the closest enclosing valid + // input type even after traversal leaves the GraphQL input type system. + ['enter', 'ObjectValue', null, 'GeoPoint', 'undefined'], + ['enter', 'ObjectField', null, 'undefined', 'GeoPoint'], + ['enter', 'Name', 'x', 'undefined', 'GeoPoint'], + ['leave', 'Name', 'x', 'undefined', 'GeoPoint'], + ['enter', 'FloatValue', null, 'undefined', 'GeoPoint'], + ['leave', 'FloatValue', null, 'undefined', 'GeoPoint'], + ['leave', 'ObjectField', null, 'undefined', 'GeoPoint'], + ['enter', 'ObjectField', null, 'undefined', 'GeoPoint'], + ['enter', 'Name', 'y', 'undefined', 'GeoPoint'], + ['leave', 'Name', 'y', 'undefined', 'GeoPoint'], + ['enter', 'FloatValue', null, 'undefined', 'GeoPoint'], + ['leave', 'FloatValue', null, 'undefined', 'GeoPoint'], + ['leave', 'ObjectField', null, 'undefined', 'GeoPoint'], + ['leave', 'ObjectValue', null, 'GeoPoint', 'undefined'], + ]); + }); + + it('supports traversals of list literals in custom scalar positions', () => { + const schema = buildSchema(` + scalar GeoPoint + `); + const ast = parseValue('[4.0, 2.0]'); + const scalarType = schema.getType('GeoPoint'); + assert(scalarType != null); + + const typeInfo = new TypeInfo(schema, scalarType); + + const visited: Array = []; + visit( + ast, + visitWithTypeInfo(typeInfo, { + enter(node) { + const type = typeInfo.getInputType(); + const parentType = typeInfo.getParentInputType(); + visited.push([ + 'enter', + node.kind, + node.kind === 'Name' ? node.value : null, + String(type), + String(parentType), + ]); + }, + leave(node) { + const type = typeInfo.getInputType(); + const parentType = typeInfo.getParentInputType(); + visited.push([ + 'leave', + node.kind, + node.kind === 'Name' ? node.value : null, + String(type), + String(parentType), + ]); + }, + }), + ); + + expect(visited).to.deep.equal([ + // Everything including ListValue should have type: undefined since the + // contents of custom scalars are not part of the GraphQL type system. + // ListValues carry the item type, so the item type is also undefined. + // getParentInputType() continues to report the closest enclosing valid + // input type even after traversal leaves the GraphQL input type system. + ['enter', 'ListValue', null, 'undefined', 'GeoPoint'], + ['enter', 'FloatValue', null, 'undefined', 'GeoPoint'], + ['leave', 'FloatValue', null, 'undefined', 'GeoPoint'], + ['enter', 'FloatValue', null, 'undefined', 'GeoPoint'], + ['leave', 'FloatValue', null, 'undefined', 'GeoPoint'], + ['leave', 'ListValue', null, 'undefined', 'GeoPoint'], + ]); + }); + it('supports traversals of fragment arguments', () => { const typeInfo = new TypeInfo(testSchema); diff --git a/src/validation/ValidationContext.ts b/src/validation/ValidationContext.ts index 83f5823c5b..5c2f86225b 100644 --- a/src/validation/ValidationContext.ts +++ b/src/validation/ValidationContext.ts @@ -285,6 +285,8 @@ export class ValidationContext extends ASTValidationContext { return this._typeInfo.getInputType(); } + // Note: continues to expose the closest enclosing valid input type if + // traversal descends into syntax with no corresponding GraphQL input type. getParentInputType(): Maybe { return this._typeInfo.getParentInputType(); } diff --git a/src/validation/__tests__/VariablesInAllowedPositionRule-test.ts b/src/validation/__tests__/VariablesInAllowedPositionRule-test.ts index f1396e9186..362b0d9195 100644 --- a/src/validation/__tests__/VariablesInAllowedPositionRule-test.ts +++ b/src/validation/__tests__/VariablesInAllowedPositionRule-test.ts @@ -534,3 +534,23 @@ describe('Validates OneOf Input Objects', () => { ]); }); }); + +describe('Non-specified behavior of variables within custom scalars', () => { + it('Allows using variables inside object literal in custom scalar', () => { + expectValid(` + query Query($x: Float) { + dog { + distanceFrom(loc: {x: $x, y: 10.0}) + } + }`); + }); + + it('Allows using variables inside list literal in custom scalar', () => { + expectValid(` + query Query($x: Float) { + dog { + distanceFrom(loc: [$x, 10.0]) + } + }`); + }); +}); diff --git a/src/validation/__tests__/harness.ts b/src/validation/__tests__/harness.ts index cb0c424a0e..c10d5f3e13 100644 --- a/src/validation/__tests__/harness.ts +++ b/src/validation/__tests__/harness.ts @@ -36,6 +36,8 @@ export const testSchema: GraphQLSchema = buildSchema(` DOWN } + scalar GeoPoint + type Dog implements Pet & Mammal & Canine { name(surname: Boolean): String nickname: String @@ -44,6 +46,7 @@ export const testSchema: GraphQLSchema = buildSchema(` doesKnowCommand(dogCommand: DogCommand): Boolean isHouseTrained(atOtherHomes: Boolean = true): Boolean isAtLocation(x: Int, y: Int): Boolean + distanceFrom(loc: GeoPoint): Float mother: Dog father: Dog }