From 8a589f6e88ecf675ce34d39d813a7b271dae46aa Mon Sep 17 00:00:00 2001 From: Adam Miskiewicz Date: Sun, 6 Sep 2015 17:32:29 -0400 Subject: [PATCH 1/3] initial subscription commit. Makes "subscription" behave like "query" --- src/execution/execute.js | 11 ++++++++- src/type/__tests__/introspection.js | 5 ++++ src/type/introspection.js | 6 +++++ src/type/schema.js | 11 +++++++++ src/utilities/TypeInfo.js | 2 ++ src/utilities/__tests__/schemaPrinter.js | 1 + src/utilities/buildASTSchema.js | 29 ++++++++++++++++-------- src/utilities/buildClientSchema.js | 8 +++++-- src/utilities/introspectionQuery.js | 2 ++ 9 files changed, 63 insertions(+), 12 deletions(-) diff --git a/src/execution/execute.js b/src/execution/execute.js index 177d1191a6..51b2f8bcba 100644 --- a/src/execution/execute.js +++ b/src/execution/execute.js @@ -235,9 +235,18 @@ function getOperationRootType( ); } return mutationType; + case 'subscription': + var subscriptionType = schema.getSubscriptionType(); + if (!subscriptionType) { + throw new GraphQLError( + 'Schema is not configured for subscriptions', + [ operation ] + ); + } + return subscriptionType; default: throw new GraphQLError( - 'Can only execute queries and mutations', + 'Can only execute queries, mutations and subscriptions', [ operation ] ); } diff --git a/src/type/__tests__/introspection.js b/src/type/__tests__/introspection.js index ece9afe178..dddff7c2d5 100644 --- a/src/type/__tests__/introspection.js +++ b/src/type/__tests__/introspection.js @@ -1190,6 +1190,11 @@ describe('Introspection', () => { description: 'If this server supports mutation, the type that ' + 'mutation operations will be rooted at.' }, + { + name: 'subscriptionType', + description: 'If this server support subscription, the type ' + + 'that subscription operations will be rooted at.', + }, { name: 'directives', description: 'A list of all directives supported by this server.' diff --git a/src/type/introspection.js b/src/type/introspection.js index 74024faad5..3608b4c4bd 100644 --- a/src/type/introspection.js +++ b/src/type/introspection.js @@ -51,6 +51,12 @@ export var __Schema = new GraphQLObjectType({ type: __Type, resolve: schema => schema.getMutationType() }, + subscriptionType: { + description: 'If this server support subscription, the type that ' + + 'subscription operations will be rooted at.', + type: __Type, + resolve: schema => schema.getSubscriptionType() + }, directives: { description: 'A list of all directives supported by this server.', type: diff --git a/src/type/schema.js b/src/type/schema.js index 4265e62e05..565976771d 100644 --- a/src/type/schema.js +++ b/src/type/schema.js @@ -58,12 +58,18 @@ export class GraphQLSchema { `Schema mutation must be Object Type if provided but ` + `got: ${config.mutation}.` ); + invariant( + !config.subscription || config.subscription instanceof GraphQLObjectType, + `Schema subscription must be Object Type if provided but ` + + `got: ${config.subscription}.` + ); this._schemaConfig = config; // Build type map now to detect any errors within this schema. this._typeMap = [ this.getQueryType(), this.getMutationType(), + this.getSubscriptionType(), __Schema ].reduce(typeMapReducer, {}); @@ -86,6 +92,10 @@ export class GraphQLSchema { return this._schemaConfig.mutation; } + getSubscriptionType(): ?GraphQLObjectType { + return this._schemaConfig.subscription; + } + getTypeMap(): TypeMap { return this._typeMap; } @@ -111,6 +121,7 @@ type TypeMap = { [typeName: string]: GraphQLType } type GraphQLSchemaConfig = { query: GraphQLObjectType; mutation?: ?GraphQLObjectType; + subscription?: ?GraphQLObjectType; } function typeMapReducer(map: TypeMap, type: ?GraphQLType): TypeMap { diff --git a/src/utilities/TypeInfo.js b/src/utilities/TypeInfo.js index 694615cae7..fed5ac3b59 100644 --- a/src/utilities/TypeInfo.js +++ b/src/utilities/TypeInfo.js @@ -127,6 +127,8 @@ export class TypeInfo { type = schema.getQueryType(); } else if (node.operation === 'mutation') { type = schema.getMutationType(); + } else if (node.operation === 'subscription') { + type = schema.getSubscriptionType(); } this._typeStack.push(type); break; diff --git a/src/utilities/__tests__/schemaPrinter.js b/src/utilities/__tests__/schemaPrinter.js index c1d50d4a38..790f335cc0 100644 --- a/src/utilities/__tests__/schemaPrinter.js +++ b/src/utilities/__tests__/schemaPrinter.js @@ -544,6 +544,7 @@ type __Schema { types: [__Type!]! queryType: __Type! mutationType: __Type + subscriptionType: __Type directives: [__Directive!]! } diff --git a/src/utilities/buildASTSchema.js b/src/utilities/buildASTSchema.js index 65de287ad4..c1d834d279 100644 --- a/src/utilities/buildASTSchema.js +++ b/src/utilities/buildASTSchema.js @@ -88,7 +88,8 @@ function getInnerTypeName(typeAST) { export function buildASTSchema( ast: Document, queryTypeName: string, - mutationTypeName: ?string + mutationTypeName: ?string, + subscriptionTypeName: ?string ): GraphQLSchema { if (isNullish(ast)) { @@ -121,6 +122,12 @@ export function buildASTSchema( ' not found in document.'); } + if (!isNullish(subscriptionTypeName) && + isNullish(astMap[subscriptionTypeName])) { + throw new Error('Specified subscription type ' + subscriptionTypeName + + ' not found in document.'); + } + /** * This generates a function that allows you to produce * type definitions on demand. We produce the function @@ -161,16 +168,20 @@ export function buildASTSchema( ast.definitions.forEach(produceTypeDef); var queryType = produceTypeDef(astMap[queryTypeName]); - var schema; - if (isNullish(mutationTypeName)) { - schema = new GraphQLSchema({ query: queryType }); - } else { - schema = new GraphQLSchema({ - query: queryType, - mutation: produceTypeDef(astMap[mutationTypeName]), - }); + + var schemaBody = { + query: queryType + }; + + if (!isNullish(mutationTypeName)) { + schemaBody.mutation = produceTypeDef(astMap[mutationTypeName]); + } + + if (!isNullish(subscriptionTypeName)) { + schemaBody.subscription = produceTypeDef(astMap[subscriptionTypeName]); } + var schema = new GraphQLSchema(schemaBody); return schema; function makeSchemaDef(def) { diff --git a/src/utilities/buildClientSchema.js b/src/utilities/buildClientSchema.js index 779d92396f..cbfefa47b0 100644 --- a/src/utilities/buildClientSchema.js +++ b/src/utilities/buildClientSchema.js @@ -312,16 +312,20 @@ export function buildClientSchema( typeIntrospection => getNamedType(typeIntrospection.name) ); - // Get the root Query and Mutation types. + // Get the root Query, Mutation, and Subscription types. var queryType = getType(schemaIntrospection.queryType); var mutationType = schemaIntrospection.mutationType ? getType(schemaIntrospection.mutationType) : null; + var subscriptionType = schemaIntrospection.subscriptionType ? + getType(schemaIntrospection.subscriptionType) : + null; // Then produce and return a Schema with these types. var schema = new GraphQLSchema({ query: (queryType: any), - mutation: (mutationType: any) + mutation: (mutationType: any), + subscription: (subscriptionType: any) }); return schema; diff --git a/src/utilities/introspectionQuery.js b/src/utilities/introspectionQuery.js index e67862ec5c..336a3fe762 100644 --- a/src/utilities/introspectionQuery.js +++ b/src/utilities/introspectionQuery.js @@ -13,6 +13,7 @@ export var introspectionQuery = ` __schema { queryType { name } mutationType { name } + subscriptionType { name } types { ...FullType } @@ -94,6 +95,7 @@ export type IntrospectionQuery = { export type IntrospectionSchema = { queryType: IntrospectionNamedTypeRef; mutationType: ?IntrospectionNamedTypeRef; + subscriptionType: ?IntrospectionNamedTypeRef; types: Array; directives: Array; } From a0c30d347568bafadd4eb8fceb72688241e06bf5 Mon Sep 17 00:00:00 2001 From: Adam Miskiewicz Date: Fri, 16 Oct 2015 14:14:35 -0400 Subject: [PATCH 2/3] code style improvements --- src/utilities/buildASTSchema.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/utilities/buildASTSchema.js b/src/utilities/buildASTSchema.js index 3ac1350b51..a4617efef0 100644 --- a/src/utilities/buildASTSchema.js +++ b/src/utilities/buildASTSchema.js @@ -181,8 +181,7 @@ export function buildASTSchema( schemaBody.subscription = produceTypeDef(astMap[subscriptionTypeName]); } - var schema = new GraphQLSchema(schemaBody); - return schema; + return new GraphQLSchema(schemaBody); function makeSchemaDef(def) { if (isNullish(def)) { From 87c6a274fb24e08a2c85a93a9294e5711eed874b Mon Sep 17 00:00:00 2001 From: Adam Miskiewicz Date: Fri, 16 Oct 2015 16:55:00 -0400 Subject: [PATCH 3/3] first pass at tests for subscriptions --- src/execution/__tests__/executor.js | 32 ++++++++- src/language/__tests__/kitchen-sink.graphql | 13 ++++ src/language/__tests__/parser.js | 25 +++++-- src/language/__tests__/printer.js | 13 ++++ src/language/__tests__/visitor.js | 67 +++++++++++++++++-- src/type/__tests__/definition.js | 35 ++++++++++ src/type/__tests__/enumType.js | 32 ++++++++- src/type/__tests__/introspection.js | 15 ++++- src/type/__tests__/validation.js | 25 +++++++ src/type/introspection.js | 2 +- src/utilities/__tests__/buildASTSchema.js | 35 +++++++++- src/utilities/__tests__/buildClientSchema.js | 16 ++++- src/utilities/__tests__/getOperationAST.js | 28 ++++++-- .../__tests__/LoneAnonymousOperation.js | 15 ++++- .../__tests__/UniqueOperationNames.js | 19 +++++- 15 files changed, 347 insertions(+), 25 deletions(-) diff --git a/src/execution/__tests__/executor.js b/src/execution/__tests__/executor.js index 84e1e9843b..f7492ab3a3 100644 --- a/src/execution/__tests__/executor.js +++ b/src/execution/__tests__/executor.js @@ -433,7 +433,7 @@ describe('Execute: Handles basic execution tasks', () => { }); it('uses the query schema for queries', async () => { - var doc = `query Q { a } mutation M { c }`; + var doc = `query Q { a } mutation M { c } subscription S { a }`; var data = { a: 'b', c: 'd' }; var ast = parse(doc); var schema = new GraphQLSchema({ @@ -448,6 +448,12 @@ describe('Execute: Handles basic execution tasks', () => { fields: { c: { type: GraphQLString }, } + }), + subscription: new GraphQLObjectType({ + name: 'S', + fields: { + a: { type: GraphQLString }, + } }) }); @@ -480,6 +486,30 @@ describe('Execute: Handles basic execution tasks', () => { expect(mutationResult).to.deep.equal({ data: { c: 'd' } }); }); + it('uses the subscription schema for subscriptions', async () => { + var doc = `query Q { a } subscription S { a }`; + var data = { a: 'b', c: 'd' }; + var ast = parse(doc); + var schema = new GraphQLSchema({ + query: new GraphQLObjectType({ + name: 'Q', + fields: { + a: { type: GraphQLString }, + } + }), + subscription: new GraphQLObjectType({ + name: 'S', + fields: { + a: { type: GraphQLString }, + } + }) + }); + + var subscriptionResult = await execute(schema, ast, data, {}, 'S'); + + expect(subscriptionResult).to.deep.equal({ data: { a: 'b' } }); + }); + it('correct field ordering despite execution order', async () => { var doc = `{ a, diff --git a/src/language/__tests__/kitchen-sink.graphql b/src/language/__tests__/kitchen-sink.graphql index 0d2ceff043..0e04e2e42d 100644 --- a/src/language/__tests__/kitchen-sink.graphql +++ b/src/language/__tests__/kitchen-sink.graphql @@ -34,6 +34,19 @@ mutation likeStory { } } +subscription StoryLikeSubscription($input: StoryLikeSubscribeInput) { + storyLikeSubscribe(input: $input) { + story { + likers { + count + } + likeSentence { + text + } + } + } +} + fragment frag on Friend { foo(size: $size, bar: $b, obj: {key: "value"}) } diff --git a/src/language/__tests__/parser.js b/src/language/__tests__/parser.js index 6cc6af056a..8e9e3ce2d8 100644 --- a/src/language/__tests__/parser.js +++ b/src/language/__tests__/parser.js @@ -163,6 +163,7 @@ fragment MissingOn Type 'fragment', 'query', 'mutation', + 'subscription', 'true', 'false' ]; @@ -185,22 +186,38 @@ fragment ${fragmentName} on Type { }); }); - it('parses experimental subscription feature', () => { + it('parses anonymous mutation operations', () => { expect(() => parse(` - subscription Foo { + mutation { + mutationField + } + `)).to.not.throw(); + }); + + it('parses anonymous subscription operations', () => { + expect(() => parse(` + subscription { subscriptionField } `)).to.not.throw(); }); - it('parses anonymous operations', () => { + it('parses named mutation operations', () => { expect(() => parse(` - mutation { + mutation Foo { mutationField } `)).to.not.throw(); }); + it('parses named subscription operations', () => { + expect(() => parse(` + subscription Foo { + subscriptionField + } + `)).to.not.throw(); + }); + it('parse creates ast', () => { var source = new Source(`{ diff --git a/src/language/__tests__/printer.js b/src/language/__tests__/printer.js index 1d25bd3791..c6216ddc4c 100644 --- a/src/language/__tests__/printer.js +++ b/src/language/__tests__/printer.js @@ -76,6 +76,19 @@ mutation likeStory { } } +subscription StoryLikeSubscription($input: StoryLikeSubscribeInput) { + storyLikeSubscribe(input: $input) { + story { + likers { + count + } + likeSentence { + text + } + } + } +} + fragment frag on Friend { foo(size: $size, bar: $b, obj: {key: "value"}) } diff --git a/src/language/__tests__/visitor.js b/src/language/__tests__/visitor.js index b191384bad..97de10b3d9 100644 --- a/src/language/__tests__/visitor.js +++ b/src/language/__tests__/visitor.js @@ -385,7 +385,63 @@ describe('Visitor', () => { [ 'leave', 'Field', 0, undefined ], [ 'leave', 'SelectionSet', 'selectionSet', 'OperationDefinition' ], [ 'leave', 'OperationDefinition', 1, undefined ], - [ 'enter', 'FragmentDefinition', 2, undefined ], + [ 'enter', 'OperationDefinition', 2, undefined ], + [ 'enter', 'Name', 'name', 'OperationDefinition' ], + [ 'leave', 'Name', 'name', 'OperationDefinition' ], + [ 'enter', 'VariableDefinition', 0, undefined ], + [ 'enter', 'Variable', 'variable', 'VariableDefinition' ], + [ 'enter', 'Name', 'name', 'Variable' ], + [ 'leave', 'Name', 'name', 'Variable' ], + [ 'leave', 'Variable', 'variable', 'VariableDefinition' ], + [ 'enter', 'NamedType', 'type', 'VariableDefinition' ], + [ 'enter', 'Name', 'name', 'NamedType' ], + [ 'leave', 'Name', 'name', 'NamedType' ], + [ 'leave', 'NamedType', 'type', 'VariableDefinition' ], + [ 'leave', 'VariableDefinition', 0, undefined ], + [ 'enter', 'SelectionSet', 'selectionSet', 'OperationDefinition' ], + [ 'enter', 'Field', 0, undefined ], + [ 'enter', 'Name', 'name', 'Field' ], + [ 'leave', 'Name', 'name', 'Field' ], + [ 'enter', 'Argument', 0, undefined ], + [ 'enter', 'Name', 'name', 'Argument' ], + [ 'leave', 'Name', 'name', 'Argument' ], + [ 'enter', 'Variable', 'value', 'Argument' ], + [ 'enter', 'Name', 'name', 'Variable' ], + [ 'leave', 'Name', 'name', 'Variable' ], + [ 'leave', 'Variable', 'value', 'Argument' ], + [ 'leave', 'Argument', 0, undefined ], + [ 'enter', 'SelectionSet', 'selectionSet', 'Field' ], + [ 'enter', 'Field', 0, undefined ], + [ 'enter', 'Name', 'name', 'Field' ], + [ 'leave', 'Name', 'name', 'Field' ], + [ 'enter', 'SelectionSet', 'selectionSet', 'Field' ], + [ 'enter', 'Field', 0, undefined ], + [ 'enter', 'Name', 'name', 'Field' ], + [ 'leave', 'Name', 'name', 'Field' ], + [ 'enter', 'SelectionSet', 'selectionSet', 'Field' ], + [ 'enter', 'Field', 0, undefined ], + [ 'enter', 'Name', 'name', 'Field' ], + [ 'leave', 'Name', 'name', 'Field' ], + [ 'leave', 'Field', 0, undefined ], + [ 'leave', 'SelectionSet', 'selectionSet', 'Field' ], + [ 'leave', 'Field', 0, undefined ], + [ 'enter', 'Field', 1, undefined ], + [ 'enter', 'Name', 'name', 'Field' ], + [ 'leave', 'Name', 'name', 'Field' ], + [ 'enter', 'SelectionSet', 'selectionSet', 'Field' ], + [ 'enter', 'Field', 0, undefined ], + [ 'enter', 'Name', 'name', 'Field' ], + [ 'leave', 'Name', 'name', 'Field' ], + [ 'leave', 'Field', 0, undefined ], + [ 'leave', 'SelectionSet', 'selectionSet', 'Field' ], + [ 'leave', 'Field', 1, undefined ], + [ 'leave', 'SelectionSet', 'selectionSet', 'Field' ], + [ 'leave', 'Field', 0, undefined ], + [ 'leave', 'SelectionSet', 'selectionSet', 'Field' ], + [ 'leave', 'Field', 0, undefined ], + [ 'leave', 'SelectionSet', 'selectionSet', 'OperationDefinition' ], + [ 'leave', 'OperationDefinition', 2, undefined ], + [ 'enter', 'FragmentDefinition', 3, undefined ], [ 'enter', 'Name', 'name', 'FragmentDefinition' ], [ 'leave', 'Name', 'name', 'FragmentDefinition' ], [ 'enter', 'NamedType', 'typeCondition', 'FragmentDefinition' ], @@ -426,8 +482,8 @@ describe('Visitor', () => { [ 'leave', 'Argument', 2, undefined ], [ 'leave', 'Field', 0, undefined ], [ 'leave', 'SelectionSet', 'selectionSet', 'FragmentDefinition' ], - [ 'leave', 'FragmentDefinition', 2, undefined ], - [ 'enter', 'OperationDefinition', 3, undefined ], + [ 'leave', 'FragmentDefinition', 3, undefined ], + [ 'enter', 'OperationDefinition', 4, undefined ], [ 'enter', 'SelectionSet', 'selectionSet', 'OperationDefinition' ], [ 'enter', 'Field', 0, undefined ], [ 'enter', 'Name', 'name', 'Field' ], @@ -450,8 +506,7 @@ describe('Visitor', () => { [ 'leave', 'Name', 'name', 'Field' ], [ 'leave', 'Field', 1, undefined ], [ 'leave', 'SelectionSet', 'selectionSet', 'OperationDefinition' ], - [ 'leave', 'OperationDefinition', 3, undefined ], - [ 'leave', 'Document', undefined, undefined ] - ]); + [ 'leave', 'OperationDefinition', 4, undefined ], + [ 'leave', 'Document', undefined, undefined ] ]); }); }); diff --git a/src/type/__tests__/definition.js b/src/type/__tests__/definition.js index 0e6dbc7d9e..5db43a210c 100644 --- a/src/type/__tests__/definition.js +++ b/src/type/__tests__/definition.js @@ -81,6 +81,16 @@ var BlogMutation = new GraphQLObjectType({ } }); +var BlogSubscription = new GraphQLObjectType({ + name: 'Subscription', + fields: { + articleSubscribe: { + args: { id: { type: GraphQLString } }, + type: BlogArticle + } + } +}); + var ObjectType = new GraphQLObjectType({ name: 'Object', isTypeOf: () => true @@ -143,6 +153,21 @@ describe('Type System: Example', () => { }); + it('defines a subscription schema', () => { + var BlogSchema = new GraphQLSchema({ + query: BlogQuery, + subscription: BlogSubscription + }); + + expect(BlogSchema.getSubscriptionType()).to.equal(BlogSubscription); + + var sub = BlogSubscription.getFields()[('articleSubscribe' : string)]; + expect(sub && sub.type).to.equal(BlogArticle); + expect(sub && sub.type.name).to.equal('Article'); + expect(sub && sub.name).to.equal('articleSubscribe'); + + }); + it('includes nested input objects in the map', () => { var NestedInputObject = new GraphQLInputObjectType({ name: 'NestedInputObject', @@ -161,9 +186,19 @@ describe('Type System: Example', () => { } } }); + var SomeSubscription = new GraphQLObjectType({ + name: 'SomeSubscription', + fields: { + subscribeToSomething: { + type: BlogArticle, + args: { input: { type: SomeInputObject } } + } + } + }); var schema = new GraphQLSchema({ query: BlogQuery, mutation: SomeMutation, + subscription: SomeSubscription }); expect(schema.getTypeMap().NestedInputObject).to.equal(NestedInputObject); }); diff --git a/src/type/__tests__/enumType.js b/src/type/__tests__/enumType.js index f8393a5736..d0a3ed5f2a 100644 --- a/src/type/__tests__/enumType.js +++ b/src/type/__tests__/enumType.js @@ -70,7 +70,22 @@ describe('Type System: Enum Values', () => { } }); - var schema = new GraphQLSchema({ query: QueryType, mutation: MutationType }); + var SubscriptionType = new GraphQLObjectType({ + name: 'Subscription', + fields: { + subscribeToEnum: { + type: ColorType, + args: { color: { type: ColorType } }, + resolve(value, { color }) { return color; } + } + } + }); + + var schema = new GraphQLSchema({ + query: QueryType, + mutation: MutationType, + subscription: SubscriptionType + }); it('accepts enum literals as input', async () => { expect( @@ -173,6 +188,21 @@ describe('Type System: Enum Values', () => { }); }); + it('accepts enum literals as input arguments to subscriptions', async () => { + expect( + await graphql( + schema, + 'subscription x($color: Color!) { subscribeToEnum(color: $color) }', + null, + { color: 'GREEN' } + ) + ).to.deep.equal({ + data: { + subscribeToEnum: 'GREEN' + } + }); + }); + it('does not accept internal value as enum variable', async () => { expect( await graphql( diff --git a/src/type/__tests__/introspection.js b/src/type/__tests__/introspection.js index 070d884bdd..52ef9dae32 100644 --- a/src/type/__tests__/introspection.js +++ b/src/type/__tests__/introspection.js @@ -42,6 +42,7 @@ describe('Introspection', () => { data: { __schema: { mutationType: null, + subscriptionType: null, queryType: { name: 'QueryRoot', }, @@ -106,6 +107,17 @@ describe('Introspection', () => { isDeprecated: false, deprecationReason: null }, + { + name: 'subscriptionType', + args: [], + type: { + kind: 'OBJECT', + name: '__Type', + ofType: null + }, + isDeprecated: false, + deprecationReason: null + }, { name: 'directives', args: [], @@ -1175,7 +1187,8 @@ describe('Introspection', () => { description: 'A GraphQL Schema defines the capabilities of a ' + 'GraphQL server. It exposes all available types and ' + 'directives on the server, as well as the entry ' + - 'points for query and mutation operations.', + 'points for query, mutation, ' + + 'and subscription operations.', fields: [ { name: 'types', diff --git a/src/type/__tests__/validation.js b/src/type/__tests__/validation.js index c9b06100b9..f46da17fd9 100644 --- a/src/type/__tests__/validation.js +++ b/src/type/__tests__/validation.js @@ -137,6 +137,20 @@ describe('Type System: A Schema must have Object root types', () => { }).not.to.throw(); }); + it('accepts a Schema whose query and subscription types are object types', () => { + expect(() => { + var SubscriptionType = new GraphQLObjectType({ + name: 'Subscription', + fields: { subscribe: { type: GraphQLString } } + }); + + return new GraphQLSchema({ + query: SomeObjectType, + subscription: SubscriptionType + }); + }).not.to.throw(); + }); + it('rejects a Schema without a query type', () => { expect( () => new GraphQLSchema({ }) @@ -164,6 +178,17 @@ describe('Type System: A Schema must have Object root types', () => { ); }); + it('rejects a Schema whose subscription type is an input type', () => { + expect( + () => new GraphQLSchema({ + query: SomeObjectType, + subscription: SomeInputObjectType + }) + ).to.throw( + 'Schema subscription must be Object Type if provided but got: SomeInputObject.' + ); + }); + }); describe('Type System: A Schema must contain uniquely named types', () => { diff --git a/src/type/introspection.js b/src/type/introspection.js index ee433be8fa..b3910a76f0 100644 --- a/src/type/introspection.js +++ b/src/type/introspection.js @@ -30,7 +30,7 @@ export var __Schema = new GraphQLObjectType({ description: 'A GraphQL Schema defines the capabilities of a GraphQL server. It ' + 'exposes all available types and directives on the server, as well as ' + - 'the entry points for query and mutation operations.', + 'the entry points for query, mutation, and subscription operations.', fields: () => ({ types: { description: 'A list of all types supported by this server.', diff --git a/src/utilities/__tests__/buildASTSchema.js b/src/utilities/__tests__/buildASTSchema.js index 14786d10cf..a49f79677a 100644 --- a/src/utilities/__tests__/buildASTSchema.js +++ b/src/utilities/__tests__/buildASTSchema.js @@ -20,9 +20,9 @@ import { buildASTSchema } from '../buildASTSchema'; * into an in-memory GraphQLSchema, and then finally * printing that GraphQL into the DSL */ -function cycleOutput(body, queryType, mutationType) { +function cycleOutput(body, queryType, mutationType, subscriptionType) { var ast = parse(body); - var schema = buildASTSchema(ast, queryType, mutationType); + var schema = buildASTSchema(ast, queryType, mutationType, subscriptionType); return '\n' + printSchema(schema); } @@ -254,6 +254,22 @@ type Mutation { expect(output).to.equal(body); }); + it('Simple type with subscription', () => { + var body = ` +type HelloScalars { + str: String + int: Int + bool: Boolean +} + +type Subscription { + subscribeHelloScalars(str: String, int: Int, bool: Boolean): HelloScalars +} +`; + var output = cycleOutput(body, 'HelloScalars', null, 'Subscription'); + expect(output).to.equal(body); + }); + it('Unreferenced type implementing referenced interface', () => { var body = ` type Concrete implements Iface { @@ -342,6 +358,21 @@ type Hello { .to.throw('Specified mutation type Wat not found in document'); }); + it('Unknown subscription type', () => { + var body = ` +type Hello { + str: String +} + +type Wat { + str: String +} +`; + var doc = parse(body); + expect(() => buildASTSchema(doc, 'Hello', 'Wat', 'Awesome')) + .to.throw('Specified subscription type Awesome not found in document'); + }); + it('Rejects query names', () => { var body = `query Foo { field }`; var doc = parse(body); diff --git a/src/utilities/__tests__/buildClientSchema.js b/src/utilities/__tests__/buildClientSchema.js index 5b8baa28d4..2473d8b6b7 100644 --- a/src/utilities/__tests__/buildClientSchema.js +++ b/src/utilities/__tests__/buildClientSchema.js @@ -62,7 +62,7 @@ describe('Type System: build schema from introspection', () => { await testSchema(schema); }); - it('builds a simple schema with a mutation type', async () => { + it('builds a simple schema with both operation types', async () => { var queryType = new GraphQLObjectType({ name: 'QueryType', description: 'This is a simple query type', @@ -88,9 +88,21 @@ describe('Type System: build schema from introspection', () => { } }); + var subscriptionType = new GraphQLObjectType({ + name: 'SubscriptionType', + description: 'This is a simple subscription type', + fields: { + string: { + type: GraphQLString, + description: 'This is a string field' + } + } + }); + var schema = new GraphQLSchema({ query: queryType, - mutation: mutationType + mutation: mutationType, + subscription: subscriptionType }); await testSchema(schema); diff --git a/src/utilities/__tests__/getOperationAST.js b/src/utilities/__tests__/getOperationAST.js index 0c1f149499..3c17551129 100644 --- a/src/utilities/__tests__/getOperationAST.js +++ b/src/utilities/__tests__/getOperationAST.js @@ -19,35 +19,53 @@ describe('getOperationAST', () => { expect(getOperationAST(doc)).to.equal(doc.definitions[0]); }); - it('Gets an operation from a document with named operation', () => { + it('Gets an operation from a document with named op (mutation)', () => { var doc = parse(`mutation Test { field }`); expect(getOperationAST(doc)).to.equal(doc.definitions[0]); }); + it('Gets an operation from a document with named op (subscription)', () => { + var doc = parse(`subscription Test { field }`); + expect(getOperationAST(doc)).to.equal(doc.definitions[0]); + }); + it('Does not get missing operation', () => { var doc = parse(`type Foo { field: String }`); expect(getOperationAST(doc)).to.equal(null); }); it('Does not get ambiguous unnamed operation', () => { - var doc = parse(`{ field } mutation Test { field }`); + var doc = parse(` + { field } + mutation Test { field } + subscription TestSub { field }`); expect(getOperationAST(doc)).to.equal(null); }); it('Does not get ambiguous named operation', () => { - var doc = parse(`query TestQ { field } mutation TestM { field }`); + var doc = parse(` + query TestQ { field } + mutation TestM { field } + subscription TestS { field }`); expect(getOperationAST(doc)).to.equal(null); }); it('Does not get misnamed operation', () => { - var doc = parse(`query TestQ { field } mutation TestM { field }`); + var doc = parse(` + query TestQ { field } + mutation TestM { field } + subscription TestS { field }`); expect(getOperationAST(doc, 'Unknown')).to.equal(null); }); it('Gets named operation', () => { - var doc = parse(`query TestQ { field } mutation TestM { field }`); + var doc = parse(` + query TestQ { field } + mutation TestM { field } + subscription TestS { field }`); expect(getOperationAST(doc, 'TestQ')).to.equal(doc.definitions[0]); expect(getOperationAST(doc, 'TestM')).to.equal(doc.definitions[1]); + expect(getOperationAST(doc, 'TestS')).to.equal(doc.definitions[2]); }); }); diff --git a/src/validation/__tests__/LoneAnonymousOperation.js b/src/validation/__tests__/LoneAnonymousOperation.js index ff86b839a6..9f66c721f0 100644 --- a/src/validation/__tests__/LoneAnonymousOperation.js +++ b/src/validation/__tests__/LoneAnonymousOperation.js @@ -77,7 +77,7 @@ describe('Validate: Anonymous operation must be alone', () => { ]); }); - it('anon operation with another operation', () => { + it('anon operation with a mutation', () => { expectFailsRule(LoneAnonymousOperation, ` { fieldA @@ -90,4 +90,17 @@ describe('Validate: Anonymous operation must be alone', () => { ]); }); + it('anon operation with a subscription', () => { + expectFailsRule(LoneAnonymousOperation, ` + { + fieldA + } + subscription Foo { + fieldB + } + `, [ + anonNotAlone(2, 7) + ]); + }); + }); diff --git a/src/validation/__tests__/UniqueOperationNames.js b/src/validation/__tests__/UniqueOperationNames.js index 0faa286f79..6a9a1a1c9f 100644 --- a/src/validation/__tests__/UniqueOperationNames.js +++ b/src/validation/__tests__/UniqueOperationNames.js @@ -69,6 +69,10 @@ describe('Validate: Unique operation names', () => { mutation Bar { field } + + subscription Baz { + field + } `); }); @@ -96,7 +100,7 @@ describe('Validate: Unique operation names', () => { ]); }); - it('multiple operations of same name of different types', () => { + it('multiple ops of same name of different types (mutation)', () => { expectFailsRule(UniqueOperationNames, ` query Foo { fieldA @@ -109,4 +113,17 @@ describe('Validate: Unique operation names', () => { ]); }); + it('multiple ops of same name of different types (subscription)', () => { + expectFailsRule(UniqueOperationNames, ` + query Foo { + fieldA + } + subscription Foo { + fieldB + } + `, [ + duplicateOp('Foo', 2, 13, 5, 20) + ]); + }); + });