Skip to content
This repository has been archived by the owner on Apr 14, 2023. It is now read-only.

Add missing validation step #241

Merged
merged 2 commits into from
Jul 31, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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)
Expand Down
27 changes: 25 additions & 2 deletions src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -57,6 +65,7 @@ export interface ServerOptions {
schema?: GraphQLSchema;
execute?: ExecuteFunction;
subscribe?: SubscribeFunction;
validationRules?: Array<(context: ValidationContext) => any>;

onOperation?: Function;
onOperationComplete?: Function;
Expand Down Expand Up @@ -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
Expand All @@ -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;
Expand Down Expand Up @@ -360,8 +371,20 @@ export class SubscriptionServer {

const document = typeof baseParams.query !== 'string' ? baseParams.query : parse(baseParams.query);
let executionIterable: AsyncIterator<ExecutionResult>;
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<ExecutionResult>(
Promise.resolve({ errors: validationErrors }),
);
} else if (this.subscribe && isASubscriptionOperation(document, params.operationName)) {
executionIterable = this.subscribe(this.schema,
document,
this.rootValue,
Expand Down
44 changes: 43 additions & 1 deletion src/test/tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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();
Expand Down