Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(server): More callbacks, clearer differences and higher extensibility #40

Merged
merged 29 commits into from
Oct 23, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
1aff2a6
fix: message type has to be a string always
enisdenjo Oct 23, 2020
10bf3ef
feat: add areGraphQLErrors and drop unused
enisdenjo Oct 23, 2020
3a9c54a
docs: why ignore
enisdenjo Oct 23, 2020
bad4172
feat: cleanup and begin with more callbacks and extensibility
enisdenjo Oct 23, 2020
6019b00
docs(protocol): refine and more explanations
enisdenjo Oct 23, 2020
7bcd100
feat: add `onOperation` and introduce operation result type
enisdenjo Oct 23, 2020
f721238
feat: all callbacks can return promises
enisdenjo Oct 23, 2020
9d1c3c0
feat: why not pass args along too
enisdenjo Oct 23, 2020
69b2aca
docs: generate
enisdenjo Oct 23, 2020
61c9d24
fix: onSubscribe test
enisdenjo Oct 23, 2020
eccc991
style: drop unused import
enisdenjo Oct 23, 2020
73c10c7
refactor: unnecessary export
enisdenjo Oct 23, 2020
75b9bf3
test: onNext behaviour
enisdenjo Oct 23, 2020
4e81b7c
test: onError behaviour
enisdenjo Oct 23, 2020
a54c11f
test: onComplete behaviour
enisdenjo Oct 23, 2020
f40eb85
test: onSubscribe returned error behaviour
enisdenjo Oct 23, 2020
143912a
test: throwing errors from callbacks
enisdenjo Oct 23, 2020
21a72b9
fix: dont forget to await
enisdenjo Oct 23, 2020
862a6dc
style: drop unused import
enisdenjo Oct 23, 2020
4fc183d
docs: custom graphql arguments recipe
enisdenjo Oct 23, 2020
62a4594
docs: persisted queries recipe (and test to make sure)
enisdenjo Oct 23, 2020
7b9199e
fix: correct tag close
enisdenjo Oct 23, 2020
385caf8
docs: remind to validate with onSubscribe
enisdenjo Oct 23, 2020
8a3c7d4
docs: static and dynamic graphql arguments
enisdenjo Oct 23, 2020
732be2b
fix: from msg payload
enisdenjo Oct 23, 2020
eef812f
style: simplifty
enisdenjo Oct 23, 2020
f683819
feat: onConnect can return nothing
enisdenjo Oct 23, 2020
2cc2032
docs: refine logging recipe
enisdenjo Oct 23, 2020
7a0414f
docs: generate [skip ci]
enisdenjo Oct 23, 2020
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
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
Loading