Skip to content

Commit

Permalink
feat: Server request handler (enisdenjo#2)
Browse files Browse the repository at this point in the history
  • Loading branch information
enisdenjo authored Aug 20, 2021
1 parent 8a51f9e commit 8381796
Show file tree
Hide file tree
Showing 16 changed files with 2,363 additions and 0 deletions.
104 changes: 104 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,110 @@
$ yarn add graphql-sse
```

#### Create a GraphQL schema

```ts
import { GraphQLSchema, GraphQLObjectType, GraphQLString } from 'graphql';

/**
* Construct a GraphQL schema and define the necessary resolvers.
*
* type Query {
* hello: String
* }
* type Subscription {
* greetings: String
* }
*/
const schema = new GraphQLSchema({
query: new GraphQLObjectType({
name: 'Query',
fields: {
hello: {
type: GraphQLString,
resolve: () => 'world',
},
},
}),
subscription: new GraphQLObjectType({
name: 'Subscription',
fields: {
greetings: {
type: GraphQLString,
subscribe: async function* () {
for (const hi of ['Hi', 'Bonjour', 'Hola', 'Ciao', 'Zdravo']) {
yield { greetings: hi };
}
},
},
},
}),
});
```

#### Start the server

##### With [`http`](https://nodejs.org/api/http.html)

```ts
import http from 'http';
import { createHandler } from 'graphql-sse';

// Create the GraphQL over SSE handler
const handler = createHandler({
schema, // from the previous step
});

// Create a HTTP server using the handler on `/graphql/stream`
const server = http.createServer((req, res) => {
if (req.url.startsWith('/graphql/stream')) return handler(req, res);
return res.writeHead(404).end();
});

server.listen(4000);
console.log('Listening to port 4000');
```

##### With [`express`](https://expressjs.com/)

```ts
import express from 'express'; // yarn add express
import { createHandler } from 'graphql-sse';

// Create the GraphQL over SSE handler
const handler = createHandler({ schema });

// Create an express app serving all methods on `/graphql/stream`
const app = express();
app.all('/graphql/stream', handler);

app.listen(4000);
console.log('Listening to port 4000');
```

##### With [`fastify`](https://www.fastify.io/)

```ts
import Fastify from 'fastify'; // yarn add fastify
import { createHandler } from 'graphql-sse';

// Create the GraphQL over SSE handler
const handler = createHandler({ schema });

// Create a fastify instance serving all methods on `/graphql/stream`
const fastify = Fastify();
fastify.all('/graphql/stream', (req, res) =>
handler(
req.raw,
res.raw,
req.body, // fastify reads the body for you
),
);

fastify.listen(4000);
console.log('Listening to port 4000');
```

## [How does it work?](PROTOCOL.md)

Read about the exact transport intricacies used by the library in the [GraphQL over Server-Sent Events Protocol document](PROTOCOL.md).
Expand Down
1 change: 1 addition & 0 deletions jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ module.exports = {
testRunner: 'jest-jasmine2',
moduleFileExtensions: ['ts', 'js'],
testRegex: '/tests/.+.ts$',
testPathIgnorePatterns: ['/node_modules/', '/utils/', '/fixtures/'],
};
7 changes: 7 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@
"test": "jest",
"release": "semantic-release"
},
"peerDependencies": {
"graphql": ">=0.11 <=15"
},
"devDependencies": {
"@babel/core": "^7.14.5",
"@babel/plugin-proposal-class-properties": "^7.14.5",
Expand All @@ -49,15 +52,19 @@
"@babel/preset-typescript": "^7.14.5",
"@semantic-release/changelog": "^5.0.1",
"@semantic-release/git": "^9.0.0",
"@types/eventsource": "^1.1.5",
"@types/jest": "^26.0.23",
"@types/node-fetch": "^2.5.12",
"@typescript-eslint/eslint-plugin": "^4.26.1",
"@typescript-eslint/parser": "^4.26.1",
"babel-jest": "^27.0.2",
"eslint": "^7.28.0",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-prettier": "^3.4.0",
"eventsource": "^1.1.0",
"graphql": "^15.5.0",
"jest": "^27.0.4",
"node-fetch": "^2.6.1",
"prettier": "^2.3.1",
"semantic-release": "^17.4.3",
"typescript": "^4.3.2"
Expand Down
80 changes: 80 additions & 0 deletions src/common.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
/**
*
* common
*
*/

import type { DocumentNode, ExecutionResult } from 'graphql';

/**
* Parameters for GraphQL's request for execution.
*
* Reference: https://github.com/graphql/graphql-over-http/blob/main/spec/GraphQLOverHTTP.md#request
*
* @category Common
*/
export interface RequestParams {
operationName?: string | undefined;
query: DocumentNode | string;
variables?: Record<string, unknown> | undefined;
extensions?: Record<string, unknown> | undefined;
}

/**
* Represents a message in an event stream.
*
* Read more: https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events#Event_stream_format
*
* @category Common
*/
export interface StreamMessage<
ForID extends boolean = false,
E extends StreamEvent = StreamEvent,
> {
// id?: string; might be used in future releases for connection recovery
event: E;
data: ForID extends true ? StreamDataForID<E> : StreamData<E>;
// retry?: number; ignored since graphql-sse implements custom retry strategies
}

/** @category Common */
export type StreamEvent = 'next' | 'complete';

/** @category Common */
export function validateStreamEvent(e: unknown): StreamEvent {
e = e as StreamEvent;
if (e !== 'next' && e !== 'complete')
throw new Error(`Invalid stream event "${e}"`);
return e;
}

/** @category Common */
export type StreamData<E extends StreamEvent = StreamEvent> = E extends 'next'
? ExecutionResult
: E extends 'complete'
? null
: never;

/** @category Common */
export type StreamDataForID<E extends StreamEvent = StreamEvent> =
E extends 'next'
? { id: string; payload: ExecutionResult }
: E extends 'complete'
? { id: string }
: never;

/** @category Common */
export function parseStreamData(e: StreamEvent, data: string): StreamData {
if (data) {
try {
data = JSON.parse(data);
} catch {
throw new Error('Invalid stream data');
}
}

if (e === 'next' && !data)
throw new Error('Stream data must be an object for "next" events');

return (data || null) as StreamData;
}
Loading

0 comments on commit 8381796

Please sign in to comment.