mswjs / msw

Industry standard API mocking for JavaScript.
https://mswjs.io
MIT License
15.86k stars 516 forks source link

Support GraphQL subscriptions #285

Open kettanaito opened 4 years ago

kettanaito commented 4 years ago

What

I suggest to add support for GraphQL subscriptions (example tutorial from Apollo).

Why

To support all three main GraphQL operations: query, mutation, and subscription.

How

  1. Research what this new feature would imply technically.
  2. Design and discuss a public API of the library for GraphQL subscriptions.
  3. Implement, document, and release.

Technical notes

hsavit1 commented 4 years ago

are there any stopgap solutions that you can recommend until this feature is built @kettanaito ?

kettanaito commented 4 years ago

@hsavit1 as far as I know, GraphQL subscription is an abstraction over WebSocket. Subscriptions support would require a WebSocket support as a pre-requisite (#156). I cannot recommend any solution to achieve that at the moment. WebSocket support is on the roadmap, but I wouldn't expect it any time soon, unless you wish to contribute to it.

Nabrok commented 2 years ago

I finally followed the advice from apollo and moved from subscription-transport-ws to graphql-ws.

Everything is working and tests even pass, but unfortunately they also spit out ...

      Unhandled GraphQL subscription error ApolloError: undefined
          at runNextTicks (node:internal/process/task_queues:61:5)
          at listOnTimeout (node:internal/timers:528:9)
          at processTimers (node:internal/timers:502:7) {
        graphQLErrors: [ Event { isTrusted: [Getter] } ],
        clientErrors: [],
        networkError: null,
        extraInfo: undefined
      }

With subscription-transport-ws I just had an unhandled GET request, which I could ignore with ...

rest.get(graphql_subscription_uri, (_req, res, ctx) => res(ctx.status(200)))

The best I could come up with so far to have graphql-ws run cleanly was to do this in the client in order to avoid sending to graphql-ws during tests ...

const link = process.env.NODE_ENV === 'test' ? httpLink : split(({ query }) => {
//const link = split(({ query }) => {
    const definition = getMainDefinition(query);
    return (
        definition.kind === 'OperationDefinition' &&
        definition.operation === 'subscription'
    );
}, wsLink, authHttpLink);

And then a handler like this ...

    graphql.operation(async (req, res, ctx) => {
        const { query } = await req.json();
        const definition = getMainDefinition(gql`${query}`);
        if (definition.kind !== 'OperationDefinition' || definition.operation !== 'subscription') {
            console.error(query);
            throw new Error("Not a subscription!");
        }
        return res(ctx.data({ all: null, possible: null, subscription: null, fields: null }));
    }),

This is kind of ugly and I don't like it. There must be a better way to just ignore subscriptions?

Nabrok commented 2 years ago

Okay, cleaner work around ...

In __mocks__/@apollo/client/link/subscriptions.ts

import { ApolloLink, FetchResult, Observable, Operation } from '@apollo/client/core';
import { print } from 'graphql';
import { Client } from 'graphql-ws';

export class GraphQLWsLink extends ApolloLink {
    constructor(private client: Client) {
        super();
    }

    public request(operation: Operation): Observable<FetchResult> {
        return new Observable((sink) => {
            return this.client.subscribe<FetchResult>(
                { ...operation, query: print(operation.query) },
                {
                    next: sink.next.bind(sink),
                    complete: sink.complete.bind(sink),
                    error: () => null
                },
            );
        });
    }
}

This is basically just the code for apollo versions < 3.5.10 but with the error function doing nothing.

Then just need the rest.get handler in my earlier post.