/graphql-websocket-router

GraphQL-WebSocket-Router

Primary LanguageTypeScript

GraphQL WebSocket Router

GraphQL WebSocket Router allows you to expose an internal GraphQL API over Websocket without exposing the entire schema.

Getting started

GraphQL Rest Router is available via NPM as graphql-websocket-router

npm install --save graphql-websocket-router

Queries (queries.gql)

# GraphQL Subscription
subscription OnEvent($topic: String) {
  onEvent(topic: $topic) {
    timestamp
    message
  }
}

# GraphQL Live Query
query GetEpisodeById($id: ID!) {
  episode(id: $id) @live {
    id
    name
    air_date
  }
}

Setting up the router/gateway with uWebSockets

import GraphQLWebsocketRouter from "graphql-websocket-router";
import { App as uWebSocketApp } from "uWebSockets.js";
import { readFileSync } from "fs";
import { resolve } from "path";

const api = new GraphQLWebsocketRouter(
  "ws://localhost:4000/graphql",
  readFileSync(resolve(__dirname, "queries.gql"), "utf-8")
);

uWebSocketApp()
  .ws("/*", api.uWebSocketBehavior())
  .listen(9001, (listenSocket) => {
    if (listenSocket) {
      console.log("Listening on port 9001");
    }
  });

WebSocket Protocol

// Example: Query
>> { method: "query", operation: "GetEpisodeById", variables: { id: 1 } }
<< { status: "ok", data: { id: 1, name: "Episode One" } }

// Example: Subscription
>> { method: "subscribe", operation: "OnEvent", variables: { topic: 'all' } }
<< { status: "ok", data: { timestamp: 1, message: "hello" } }
<< { status: "update", data: { timestamp: 2, message: "world" } }
<< { status: "update", data: { timestamp: 3, message: "!" } }

// Example: Live Subscribe
>> { method: "subscribe", operation: "GetEpisodeById", variables: { id: 1 } }
<< { status: "ok", data: { id: 1, name: "Episode O" } }
<< { status: "update", data: { name: "Episode On" } }
<< { status: "update", data: { name: "Episode One" } }

// Example: Unsubscribe
>> { method: "unsubscribe", operation: "GetEpisodeById", variables: { id: 1 } }
<< { status: "ok" }

// Example: Error
>> { method: "query", operation: "GetEpisodeById", /* missing id */ }
<< { status: "error", error: { message: "id is missing" } }

Supported backends

Connecting to WebSocket backend

Supports live queries, subscriptions, queries and mutations.

// ...

const api = new GraphQLWebsocketRouter(
  "ws://localhost:4000/graphql",
  readFileSync(resolve(__dirname, "queries.gql"), "utf-8"),
  { enableDeltaPatching: false }
);

// ...

Connecting to HTTP backend

Supports queries and mutations only.

// ...

const api = new GraphQLWebsocketRouter(
  "https://rickandmortyapi.com/graphql",
  readFileSync(resolve(__dirname, "queries.gql"), "utf-8")
);

// ...

Client Examples

Vanilla

const socket = new WebSocket("ws://localhost:9001");

socket.onopen = (event) => {
  // connection established
  socket.send(
    JSON.stringify({
      method: "subscribe",
      operation: "GetEpisodeById",
      variables: { id: 1 },
    })
  );
};

socket.onmessage = (event) => {
  const { status, data: document } = JSON.parse(event.data);
  console.log("[received]", document);
};

socket.onerror = (error) => {
  // handle error
};

React Hook (TBD)

import { useLiveQuery } from "graphql-websocket-router/client/react";

export default function MyComponent() {
  const document = useLiveQuery({
    operation: "GetEpisodeById",
    variables: { id: 1 },
  });

  return <pre>{JSON.stringify(document)}</pre>;
}

Next.js (TBD)

import {
  fetchLiveQuery,
  useLiveQuery,
} from "graphql-websocket-router/client/react";

export async function getStaticProps(context) {
  const { query: episodeQuery } = await fetchLiveQuery({
    operation: "GetEpisodeById",
    variables: { id: 1 },
  });

  return {
    props: { episodeQuery },
  };
}

export default function EpisodeDetails({ episodeQuery }) {
  const episode = useLiveQuery(episodeQuery);

  // Render episode...
}