diff --git a/src/execution/__tests__/variables-test.ts b/src/execution/__tests__/variables-test.ts index b579c249ce..4e702112c1 100644 --- a/src/execution/__tests__/variables-test.ts +++ b/src/execution/__tests__/variables-test.ts @@ -168,6 +168,7 @@ const TestType = new GraphQLObjectType({ }), fieldWithJSONScalarInput: fieldWithInputArg({ type: TestJSONScalar }), list: fieldWithInputArg({ type: new GraphQLList(GraphQLString) }), + listJSON: fieldWithInputArg({ type: new GraphQLList(TestJSONScalar) }), nested: { type: NestedType, resolve: () => ({}), @@ -986,6 +987,41 @@ describe('Execute: Handles inputs', () => { expect(result).to.deep.equal({ data: { list: '["A"]' } }); }); + it('coerces a single element to a one-element list', () => { + const doc = ` + query ($input: [String]) { + list(input: $input) + } + `; + const result = executeQuery(doc, { input: 'A' }); + + expect(result).to.deep.equal({ data: { list: '["A"]' } }); + }); + + it('allows lists to contain custom scalars', () => { + const doc = ` + query ($input: [JSONScalar]) { + listJSON(input: $input) + } + `; + const result = executeQuery(doc, { input: [{ a: 1 }, { b: 2 }] }); + + expect(result).to.deep.equal({ + data: { listJSON: '[{ a: 1 }, { b: 2 }]' }, + }); + }); + + it('coerces a single element of custom scalar into a one-element list', () => { + const doc = ` + query ($input: [JSONScalar]) { + listJSON(input: $input) + } + `; + const result = executeQuery(doc, { input: { a: 1 } }); + + expect(result).to.deep.equal({ data: { listJSON: '[{ a: 1 }]' } }); + }); + it('allows lists to contain null', () => { const doc = ` query ($input: [String]) { diff --git a/src/utilities/TypeInfo.ts b/src/utilities/TypeInfo.ts index 22255faf64..56ef33a892 100644 --- a/src/utilities/TypeInfo.ts +++ b/src/utilities/TypeInfo.ts @@ -254,7 +254,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..75807552ad 100644 --- a/src/utilities/__tests__/TypeInfo-test.ts +++ b/src/utilities/__tests__/TypeInfo-test.ts @@ -451,10 +451,10 @@ describe('visitWithTypeInfo', () => { ['enter', 'ObjectField', null, '[String]'], ['enter', 'Name', 'stringListField', '[String]'], ['leave', 'Name', 'stringListField', '[String]'], - ['enter', 'ListValue', null, 'String'], + ['enter', 'ListValue', null, 'String' /* the item type, not list type */], ['enter', 'StringValue', null, 'String'], ['leave', 'StringValue', null, 'String'], - ['leave', 'ListValue', null, 'String'], + ['leave', 'ListValue', null, 'String' /* the item type, not list type */], ['leave', 'ObjectField', null, '[String]'], ['leave', 'ObjectValue', null, 'ComplexInput'], ]); @@ -735,4 +735,104 @@ describe('visitWithTypeInfo', () => { ['leave', 'Document', null, 'undefined', 'undefined'], ]); }); + + it('supports traversals of object literals of custom scalars', () => { + 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(); + visited.push([ + 'enter', + node.kind, + node.kind === 'Name' ? node.value : null, + String(type), + ]); + }, + leave(node) { + const type = typeInfo.getInputType(); + visited.push([ + 'leave', + node.kind, + node.kind === 'Name' ? node.value : null, + String(type), + ]); + }, + }), + ); + + expect(visited).to.deep.equal([ + // Everything within ObjectValue should have type: undefined since the + // contents of custom scalars aren't part of GraphQL schema definitions. + ['enter', 'ObjectValue', null, 'GeoPoint'], + ['enter', 'ObjectField', null, 'undefined'], + ['enter', 'Name', 'x', 'undefined'], + ['leave', 'Name', 'x', 'undefined'], + ['enter', 'FloatValue', null, 'undefined'], + ['leave', 'FloatValue', null, 'undefined'], + ['leave', 'ObjectField', null, 'undefined'], + ['enter', 'ObjectField', null, 'undefined'], + ['enter', 'Name', 'y', 'undefined'], + ['leave', 'Name', 'y', 'undefined'], + ['enter', 'FloatValue', null, 'undefined'], + ['leave', 'FloatValue', null, 'undefined'], + ['leave', 'ObjectField', null, 'undefined'], + ['leave', 'ObjectValue', null, 'GeoPoint'], + ]); + }); + + it('supports traversals of list literals of custom scalars', () => { + 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(); + visited.push([ + 'enter', + node.kind, + node.kind === 'Name' ? node.value : null, + String(type), + ]); + }, + leave(node) { + const type = typeInfo.getInputType(); + visited.push([ + 'leave', + node.kind, + node.kind === 'Name' ? node.value : null, + String(type), + ]); + }, + }), + ); + + expect(visited).to.deep.equal([ + ['enter', 'ListValue', null, 'undefined'], + ['enter', 'FloatValue', null, 'undefined'], + ['leave', 'FloatValue', null, 'undefined'], + ['enter', 'FloatValue', null, 'undefined'], + ['leave', 'FloatValue', null, 'undefined'], + ['leave', 'ListValue', null, 'undefined'], + ]); + }); }); diff --git a/src/validation/__tests__/VariablesInAllowedPositionRule-test.ts b/src/validation/__tests__/VariablesInAllowedPositionRule-test.ts index 127f146230..214e711c6e 100644 --- a/src/validation/__tests__/VariablesInAllowedPositionRule-test.ts +++ b/src/validation/__tests__/VariablesInAllowedPositionRule-test.ts @@ -492,4 +492,81 @@ describe('Validate: Variables are in allowed positions', () => { ]); }); }); + + it('Custom scalars as arg', () => { + expectValid(` + query Query($point: GeoPoint) { + dog { + distanceFrom(loc: $point) + } + }`); + }); + + it('Forbids using custom scalar as builtin arg', () => { + expectErrors(` + query Query($point: GeoPoint) { + dog { + isAtLocation(x: $point, y: 10) + } + } + `).toDeepEqual([ + { + locations: [ + { + column: 19, + line: 2, + }, + { + column: 27, + line: 4, + }, + ], + message: + 'Variable "$point" of type "GeoPoint" used in position expecting type "Int".', + }, + ]); + }); + + it('Forbids using builtin scalar as custom scalar arg', () => { + expectErrors(` + query Query($x: Float) { + dog { + distanceFrom(loc: $x) + } + } + `).toDeepEqual([ + { + locations: [ + { + column: 19, + line: 2, + }, + { + column: 29, + line: 4, + }, + ], + message: + 'Variable "$x" of type "Float" used in position expecting type "GeoPoint".', + }, + ]); + }); + + 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 }