Skip to content

Commit

Permalink
feat(server): More callbacks, clearer differences and higher extensib…
Browse files Browse the repository at this point in the history
…ility (#40)

BREAKING CHANGES

This time we come with a few breaking changes that will open doors for all sorts of enhancements. Check the linked PR for more details.

### Server option `onSubscribe`
- Now executes _before_ any other subscribe message processing
- Now takes 2 arguments, the `Context` and the `SubscribeMessage`
- Now returns nothing,`ExecutionArgs` or an array of `GraphQLError`s
  - Returning `void` (or nothing) will leave the execution args preparation and validation to the library
  - Returned `ExecutionArgs` will be used **directly** for the GraphQL operation execution (preparations and validation should be done by you in this case)
  - Returned array of `GraphQLError`s will be reported to the client through the `ErrorMessage`

### Server option `validationRules`
Dropped in favour of applying custom validation rules in the `onSubscribe` callback. Find the recipe in the readme!

### Server option `formatExecutionResult`
Dropped in favour of using the return value of `onNext` callback.
  • Loading branch information
enisdenjo committed Oct 23, 2020
1 parent 92723ed commit 507a222
Show file tree
Hide file tree
Showing 10 changed files with 1,011 additions and 344 deletions.
34 changes: 26 additions & 8 deletions PROTOCOL.md
Original file line number Diff line number Diff line change
Expand Up @@ -195,11 +195,19 @@ _The client and the server has already gone through [successful connection initi
1. _Client_ generates a unique ID for the following operation
1. _Client_ dispatches the `Subscribe` message with the generated ID through the `id` field and the requested operation passed through the `payload` field
<br>_All future communication is linked through this unique ID_
1. _Server_ triggers the `onSubscribe` callback, if specified, and uses the returned `ExecutionArgs` for the operation
1. _Server_ validates the request and executes the single result GraphQL operation
1. _Server_ dispatches the `Next` message with the execution result
1. _Server_ triggers the `onSubscribe` callback

- If `ExecutionArgs` are **not** returned, the arguments will be formed and validated using the payload
- If `ExecutionArgs` are returned, they will be used directly

1. _Server_ executes the single result GraphQL operation using the arguments provided above
1. _Server_ triggers the `onNext` callback

- If `ExecutionResult` is **not** returned, the direct result from the operation will be dispatched with the `Next` message
- If `ExecutionResult` is returned, it will be dispatched with the `Next` message

1. _Server_ triggers the `onComplete` callback
1. _Server_ dispatches the `Complete` message indicating that the execution has completed
1. _Server_ triggers the `onComplete` callback, if specified

### Streaming operation

Expand All @@ -208,14 +216,24 @@ _The client and the server has already gone through [successful connection initi
_The client and the server has already gone through [successful connection initialisation](#successful-connection-initialisation)._

1. _Client_ generates a unique ID for the following operation
1. _Client_ dispatches the `Subscribe` message with the generated ID through the `id` field and the requested streaming operation passed through the `payload` field
1. _Client_ dispatches the `Subscribe` message with the generated ID through the `id` field and the requested operation passed through the `payload` field
<br>_All future communication is linked through this unique ID_
1. _Server_ triggers the `onSubscribe` callback, if specified, and uses the returned `ExecutionArgs` for the operation
1. _Server_ validates the request and executes the streaming GraphQL operation
1. _Server_ triggers the `onSubscribe` callback

- If `ExecutionArgs` are **not** returned, the arguments will be formed and validated using the payload
- If `ExecutionArgs` are returned, they will be used directly

1. _Server_ executes the streaming GraphQL operation using the arguments provided above
1. _Server_ checks if the generated ID is unique across active streaming subscriptions

- If **not** unique, the _server_ will close the socket with the event `4409: Subscriber for <generated-id> already exists`
- If unique, continue...
1. _Server_ dispatches `Next` messages for every event in the source stream

1. _Server_ triggers the `onNext` callback

- If `ExecutionResult` is **not** returned, the direct events from the source stream will be dispatched with the `Next` message
- If `ExecutionResult` is returned, it will be dispatched with the `Next` message instead of every event from the source stram

1. - _Client_ stops the subscription by dispatching a `Complete` message
- _Server_ completes the source stream
<br>_or_
Expand Down
164 changes: 135 additions & 29 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -400,29 +400,20 @@ const server = https.createServer(function weServeSocketsOnly(_, res) {
createServer(
{
schema,
execute: async (args) => {
console.log('Execute', args);
const result = await execute(args);
console.debug('Execute result', result);
return result;
},
subscribe: async (args) => {
console.log('Subscribe', args);
const subscription = await subscribe(args);
// NOTE: `subscribe` can sometimes return a single result, I dont consider it here for sake of simplicity
return (async function* () {
for await (const result of subscription) {
console.debug('Subscribe yielded result', { args, result });
yield result;
}
})();
},
onConnect: (ctx) => {
console.log('Connect', ctx);
return true; // default behaviour - permit all connection attempts
},
onSubscribe: (ctx, msg) => {
console.log('Subscribe', { ctx, msg });
},
onNext: (ctx, msg, args, result) => {
console.debug('Next', { ctx, msg, args, result });
},
onError: (ctx, msg, errors) => {
console.error('Error', { ctx, msg, errors });
},
onComplete: (ctx, msg) => {
console.debug('Complete', { ctx, msg });
console.log('Complete', { ctx, msg });
},
},
{
Expand Down Expand Up @@ -498,25 +489,58 @@ server.listen(443);
</details>

<details>
<summary>Server usage with a custom GraphQL context</summary>
<summary>Server usage with custom static GraphQL arguments</summary>

```typescript
import { execute, subscribe } from 'graphql';
import { validate, execute, subscribe } from 'graphql';
import { createServer } from 'graphql-transport-ws';
import { schema } from 'my-graphql-schema';
import { schema, roots, getStaticContext } from 'my-graphql';

createServer(
{
context: getStaticContext(),
schema,
roots,
execute,
subscribe,
onSubscribe: (ctx, msg, args) => {
return [
{
...args,
contextValue: getCustomContext(ctx, msg, args),
},
];
},
{
server,
path: '/graphql',
},
);
```

</details>

<details>
<summary>Server usage with custom dynamic GraphQL arguments and validation</summary>

```typescript
import { parse, validate, execute, subscribe } from 'graphql';
import { createServer } from 'graphql-transport-ws';
import { schema, getDynamicContext, myValidationRules } from 'my-graphql';

createServer(
{
execute,
subscribe,
onSubscribe: (ctx, msg) => {
const args = {
schema,
contextValue: getDynamicContext(ctx, msg),
operationName: msg.payload.operationName,
document: parse(msg.payload.operationName),
variableValues: msg.payload.variables,
};

// dont forget to validate when returning custom execution args!
const errors = validate(args.schema, args.document, myValidationRules);
if (errors.length > 0) {
return errors; // return `GraphQLError[]` to send `ErrorMessage` and stop subscription
}

return args;
},
},
{
Expand All @@ -528,6 +552,88 @@ createServer(

</details>

<details>
<summary>Server and client usage with persisted queries</summary>

```typescript
// 🛸 server

import { parse, execute, subscribe } from 'graphql';
import { createServer } from 'graphql-transport-ws';
import { schema } from 'my-graphql-schema';

type QueryID = string;

const queriesStore: Record<QueryID, ExecutionArgs> = {
iWantTheGreetings: {
schema, // you may even provide different schemas in the queries store
document: parse('subscription Greetings { greetings }'),
},
};

createServer(
{
execute,
subscribe,
onSubscribe: (_ctx, msg) => {
// search using `SubscriptionPayload.query` as QueryID
// check the client example below for better understanding
const hit = queriesStore[msg.payload.query];
if (hit) {
return {
...hit,
variableValues: msg.payload.variables, // use the variables from the client
};
}
// if no hit, execute as usual
return {
schema,
operationName: msg.payload.operationName,
document: parse(msg.payload.operationName),
variableValues: msg.payload.variables,
};
},
},
{
server,
path: '/graphql',
},
);
```

```typescript
// 📺 client

import { createClient } from 'graphql-transport-ws';

const client = createClient({
url: 'wss://persisted.graphql/queries',
});

(async () => {
const onNext = () => {
/**/
};

await new Promise((resolve, reject) => {
client.subscribe(
{
query: 'iWantTheGreetings',
},
{
next: onNext,
error: reject,
complete: resolve,
},
);
});

expect(onNext).toBeCalledTimes(5); // greetings in 5 languages
})();
```

</details>

## [Documentation](docs/)

Check the [docs folder](docs/) out for [TypeDoc](https://typedoc.org) generated documentation.
Expand Down

0 comments on commit 507a222

Please sign in to comment.