diff --git a/CHANGELOG.md b/CHANGELOG.md index 57661775b..b44b11603 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,12 +1,13 @@ # Changelog ### vNEXT +- Add Validation step to server [PR #241](https://github.com/apollographql/subscriptions-transport-ws/pull/241) +- Call operation handler before delete the operation on operation complete [PR #239](https://github.com/apollographql/subscriptions-transport-ws/pull/239) ### 0.8.1 - Send first keep alive message right after the ack [PR #223](https://github.com/apollographql/subscriptions-transport-ws/pull/223) - Return after first post-install when it should install dev dependencies [PR #218](https://github.com/apollographql/subscriptions-transport-ws/pull/218) - On installing from branch install dev dependencies only if dist folder isnt found [PR #219](https://github.com/apollographql/subscriptions-transport-ws/pull/219) -- Call operation handler before delete the operation on operation complete [PR #239](https://github.com/apollographql/subscriptions-transport-ws/pull/239) ### 0.8.0 - Expose opId `onOperationComplete` method [PR #211](https://github.com/apollographql/subscriptions-transport-ws/pull/211) diff --git a/src/server.ts b/src/server.ts index 10aead6a1..f15e7ca6f 100644 --- a/src/server.ts +++ b/src/server.ts @@ -4,7 +4,15 @@ import MessageTypes from './message-types'; import { GRAPHQL_WS, GRAPHQL_SUBSCRIPTIONS } from './protocol'; import { SubscriptionManager } from 'graphql-subscriptions'; import isObject = require('lodash.isobject'); -import { parse, ExecutionResult, GraphQLSchema, DocumentNode } from 'graphql'; +import { + parse, + ExecutionResult, + GraphQLSchema, + DocumentNode, + validate, + ValidationContext, + specifiedRules, +} from 'graphql'; import { executeFromSubscriptionManager } from './adapters/subscription-manager'; import { createEmptyIterable } from './utils/empty-iterable'; import { createAsyncIterator, forAwaitEach, isAsyncIterable } from 'iterall'; @@ -57,6 +65,7 @@ export interface ServerOptions { schema?: GraphQLSchema; execute?: ExecuteFunction; subscribe?: SubscribeFunction; + validationRules?: Array<(context: ValidationContext) => any>; onOperation?: Function; onOperationComplete?: Function; @@ -90,6 +99,7 @@ export class SubscriptionServer { private schema: GraphQLSchema; private rootValue: any; private keepAlive: number; + private specifiedRules: Array<(context: ValidationContext) => any>; /** * @deprecated onSubscribe is deprecated, use onOperation instead @@ -110,6 +120,7 @@ export class SubscriptionServer { onOperationComplete, onConnect, onDisconnect, keepAlive, } = options; + this.specifiedRules = options.validationRules || specifiedRules; this.loadExecutor(options); this.onOperation = onSubscribe ? onSubscribe : onOperation; @@ -360,8 +371,20 @@ export class SubscriptionServer { const document = typeof baseParams.query !== 'string' ? baseParams.query : parse(baseParams.query); let executionIterable: AsyncIterator; + let validationErrors: Error[] = []; - if (this.subscribe && isASubscriptionOperation(document, params.operationName)) { + if ( this.schema ) { + // NOTE: This is a temporary condition to support the legacy subscription manager. + // As soon as subscriptionManager support is removed, we can remove the if + // and keep only the validation part. + validationErrors = validate(this.schema, document, this.specifiedRules); + } + + if ( validationErrors.length > 0 ) { + executionIterable = createIterableFromPromise( + Promise.resolve({ errors: validationErrors }), + ); + } else if (this.subscribe && isASubscriptionOperation(document, params.operationName)) { executionIterable = this.subscribe(this.schema, document, this.rootValue, diff --git a/src/test/tests.ts b/src/test/tests.ts index c1fd2a86d..d9a24c847 100644 --- a/src/test/tests.ts +++ b/src/test/tests.ts @@ -14,7 +14,7 @@ import { } from 'chai'; import * as sinon from 'sinon'; import * as WebSocket from 'ws'; -import { execute, subscribe } from 'graphql'; +import { specifiedRules, execute, subscribe } from 'graphql'; Object.assign(global, { WebSocket: WebSocket, @@ -2033,6 +2033,48 @@ describe('Client<->Server Flow', () => { }); }); + it('validate requests against schema', (done) => { + const testServer = createServer(notFoundRequestListener); + testServer.listen(SERVER_EXECUTOR_TESTS_PORT); + + SubscriptionServer.create({ + schema: subscriptionsSchema, + execute, + subscribe, + validationRules: specifiedRules, + }, { + server: testServer, + path: '/', + }); + + const client = new SubscriptionClient(`ws://localhost:${SERVER_EXECUTOR_TESTS_PORT}/`); + let isFirstTime = true; + + client.onConnected(async () => { + // Manually close the connection only in the first time, to avoid infinite loop + if (isFirstTime) { + isFirstTime = false; + + setTimeout(() => { + // Disconnect the client + client.close(false); + + // Subscribe to data, without manually reconnect before + const opId = client.subscribe({ + query: `query { invalid }`, + variables: {}, + }, (err, res) => { + expect(opId).not.to.eq(null); + expect(res).to.eq(null); + expect(err[0].message).to.eq('Cannot query field "invalid" on type "Query".'); + testServer.close(); + done(); + }); + }, 300); + } + }); + }); + it('should close iteration over AsyncIterator when client unsubscribes', async () => { subscriptionAsyncIteratorSpy.reset(); resolveAsyncIteratorSpy.reset();