Skip to content

Commit 8d30500

Browse files
committed
add subscribepayload to hooks and change context and schema arguments
1 parent 6ffc3a3 commit 8d30500

File tree

4 files changed

+125
-56
lines changed

4 files changed

+125
-56
lines changed

.changeset/strange-ties-mix.md

Lines changed: 53 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,50 @@
22
'graphql-ws': major
33
---
44

5-
`onSubscribe`, `onOperation`, `onError`, `onNext` and `onComplete` hooks don't have the full accompanying message anymore, only the ID and the relevant part from the message
5+
`schema`, `context`, `onSubscribe`, `onOperation`, `onError`, `onNext` and `onComplete` hooks don't have the full accompanying message anymore, only the ID and the relevant part from the message
66

77
There is really no need to pass the full `SubscribeMessage` to the `onSubscribe` hook. The only relevant parts from the message are the `id` and the `payload`, the `type` is useless since the hook inherently has it (`onNext` is `next` type, `onError` is `error` type, etc).
88

99
The actual techincal reason for not having the full message is to avoid serialising results and errors twice. Both `onNext` and `onError` allow the user to augment the result and return it to be used instead. `onNext` originally had the `NextMessage` argument which already has the `FormattedExecutionResult`, and `onError` originally had the `ErrorMessage` argument which already has the `GraphQLFormattedError`, and they both also returned `FormattedExecutionResult` and `GraphQLFormattedError` respectivelly - meaning, if the user serialised the results - the serialisation would happen **twice**.
1010

11+
Additionally, the `onOperation`, `onError`, `onNext` and `onComplete` now have the `payload` which is the `SubscribeMessage.payload` (`SubscribePayload`) for easier access to the original query as well as execution params extensions.
12+
1113
### Migrating from v5 to v6
1214

15+
#### `schema`
16+
17+
```diff
18+
import { ServerOptions, SubscribePayload } from 'graphql-ws';
19+
20+
const opts: ServerOptions = {
21+
- schema(ctx, message) {
22+
- const messageId = message.id;
23+
- const messagePayload: SubscribePayload = message.payload;
24+
- },
25+
+ schema(ctx, id, payload) {
26+
+ const messageId = id;
27+
+ const messagePayload: SubscribePayload = payload;
28+
+ },
29+
};
30+
```
31+
32+
#### `context`
33+
34+
```diff
35+
import { ServerOptions, SubscribePayload } from 'graphql-ws';
36+
37+
const opts: ServerOptions = {
38+
- context(ctx, message) {
39+
- const messageId = message.id;
40+
- const messagePayload: SubscribePayload = message.payload;
41+
- },
42+
+ context(ctx, id, payload) {
43+
+ const messageId = id;
44+
+ const messagePayload: SubscribePayload = payload;
45+
+ },
46+
};
47+
```
48+
1349
#### `onSubscribe`
1450

1551
```diff
@@ -40,9 +76,9 @@ const opts: ServerOptions = {
4076
- const messageId = message.id;
4177
- const messagePayload: SubscribePayload = message.payload;
4278
- },
43-
+ onOperation(ctx, id, args) {
79+
+ onOperation(ctx, id, payload) {
4480
+ const messageId = id;
45-
+ const executionArgs: ExecutionArgs = args;
81+
+ const messagePayload: SubscribePayload = payload;
4682
+ },
4783
};
4884
```
@@ -53,18 +89,19 @@ The `ErrorMessage.payload` (`GraphQLFormattedError[]`) is not useful here at all
5389

5490
```diff
5591
import { GraphQLError, GraphQLFormattedError } from 'graphql';
56-
import { ServerOptions } from 'graphql-ws';
92+
import { ServerOptions, SubscribePayload } from 'graphql-ws';
5793

5894
const opts: ServerOptions = {
5995
- onError(ctx, message, errors) {
6096
- const messageId = message.id;
6197
- const graphqlErrors: readonly GraphQLError[] = errors;
62-
- const messagePayload: readonly GraphQLFormattedError[] = message.payload;
98+
- const errorMessagePayload: readonly GraphQLFormattedError[] = message.payload;
6399
- },
64-
+ onError(ctx, id, errors) {
100+
+ onError(ctx, id, payload, errors) {
65101
+ const messageId = id;
66102
+ const graphqlErrors: readonly GraphQLError[] = errors;
67-
+ const messagePayload: readonly GraphQLFormattedError[] = errors.map((e) => e.toJSON());
103+
+ const subscribeMessagePayload: SubscribePayload = payload;
104+
+ const errorMessagePayload: readonly GraphQLFormattedError[] = errors.map((e) => e.toJSON());
68105
+ },
69106
};
70107
```
@@ -75,33 +112,35 @@ The `NextMessage.payload` (`FormattedExecutionResult`) is not useful here at all
75112

76113
```diff
77114
import { ExecutionResult, FormattedExecutionResult } from 'graphql';
78-
import { ServerOptions } from 'graphql-ws';
115+
import { ServerOptions, SubscribePayload } from 'graphql-ws';
79116

80117
const opts: ServerOptions = {
81-
- onNext(ctx, message, result) {
118+
- onNext(ctx, message, _args, result) {
82119
- const messageId = message.id;
83120
- const graphqlResult: ExecutionResult = result;
84-
- const messagePayload: FormattedExecutionResult = message.payload;
121+
- const nextMessagePayload: FormattedExecutionResult = message.payload;
85122
- },
86-
+ onNext(ctx, id, result) {
123+
+ onNext(ctx, id, payload, _args, result) {
87124
+ const messageId = id;
88125
+ const graphqlResult: ExecutionResult = result;
89-
+ const messagePayload: FormattedExecutionResult = { ...result, errors: result.errors?.map((e) => e.toJSON()) };
126+
+ const subscribeMessagePayload: SubscribePayload = payload;
127+
+ const nextMessagePayload: FormattedExecutionResult = { ...result, errors: result.errors?.map((e) => e.toJSON()) };
90128
+ },
91129
};
92130
```
93131

94132
#### `onComplete`
95133

96134
```diff
97-
import { ServerOptions } from 'graphql-ws';
135+
import { ServerOptions, SubscribePayload } from 'graphql-ws';
98136

99137
const opts: ServerOptions = {
100138
- onComplete(ctx, message) {
101139
- const messageId = message.id;
102140
- },
103-
+ onComplete(ctx, id) {
141+
+ onComplete(ctx, id, payload) {
104142
+ const messageId = id;
143+
+ const subscribeMessagePayload: SubscribePayload = payload;
105144
+ },
106145
};
107146
```

src/server.ts

Lines changed: 63 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,8 @@ export interface ServerOptions<
9696
| GraphQLSchema
9797
| ((
9898
ctx: Context<P, E>,
99-
message: SubscribeMessage,
99+
id: string,
100+
payload: SubscribePayload,
100101
args: Omit<ExecutionArgs, 'schema'>,
101102
) => Promise<GraphQLSchema> | GraphQLSchema);
102103
/**
@@ -123,7 +124,8 @@ export interface ServerOptions<
123124
| GraphQLExecutionContextValue
124125
| ((
125126
ctx: Context<P, E>,
126-
message: SubscribeMessage,
127+
id: string,
128+
payload: SubscribePayload,
127129
args: ExecutionArgs,
128130
) =>
129131
| Promise<GraphQLExecutionContextValue>
@@ -333,6 +335,7 @@ export interface ServerOptions<
333335
| ((
334336
ctx: Context<P, E>,
335337
id: string,
338+
payload: SubscribePayload,
336339
args: ExecutionArgs,
337340
result: OperationResult,
338341
) => Promise<OperationResult | void> | OperationResult | void);
@@ -354,6 +357,7 @@ export interface ServerOptions<
354357
| ((
355358
ctx: Context<P, E>,
356359
id: string,
360+
payload: SubscribePayload,
357361
errors: readonly GraphQLError[],
358362
) =>
359363
| Promise<readonly GraphQLFormattedError[] | void>
@@ -378,6 +382,7 @@ export interface ServerOptions<
378382
| ((
379383
ctx: Context<P, E>,
380384
id: string,
385+
payload: SubscribePayload,
381386
args: ExecutionArgs,
382387
result: ExecutionResult | ExecutionPatchResult,
383388
) =>
@@ -402,7 +407,11 @@ export interface ServerOptions<
402407
*/
403408
onComplete?:
404409
| undefined
405-
| ((ctx: Context<P, E>, id: string) => Promise<void> | void);
410+
| ((
411+
ctx: Context<P, E>,
412+
id: string,
413+
payload: SubscribePayload,
414+
) => Promise<void> | void);
406415
/**
407416
* An optional override for the JSON.parse function used to hydrate
408417
* incoming messages to this server. Useful for parsing custom datatypes
@@ -690,10 +699,17 @@ export function makeServer<
690699
const emit = {
691700
next: async (
692701
result: ExecutionResult | ExecutionPatchResult,
702+
{ id, payload }: SubscribeMessage,
693703
args: ExecutionArgs,
694704
) => {
695705
const { errors, ...resultWithoutErrors } = result;
696-
const maybeResult = await onNext?.(ctx, id, args, result);
706+
const maybeResult = await onNext?.(
707+
ctx,
708+
id,
709+
payload,
710+
args,
711+
result,
712+
);
697713
await socket.send(
698714
stringifyMessage<MessageType.Next>(
699715
{
@@ -711,8 +727,11 @@ export function makeServer<
711727
),
712728
);
713729
},
714-
error: async (errors: readonly GraphQLError[]) => {
715-
const maybeErrors = await onError?.(ctx, id, errors);
730+
error: async (
731+
errors: readonly GraphQLError[],
732+
{ id, payload }: SubscribeMessage,
733+
) => {
734+
const maybeErrors = await onError?.(ctx, id, payload, errors);
716735
await socket.send(
717736
stringifyMessage<MessageType.Error>(
718737
{
@@ -724,8 +743,11 @@ export function makeServer<
724743
),
725744
);
726745
},
727-
complete: async (notifyClient: boolean) => {
728-
await onComplete?.(ctx, id);
746+
complete: async (
747+
notifyClient: boolean,
748+
{ id, payload }: SubscribeMessage,
749+
) => {
750+
await onComplete?.(ctx, id, payload);
729751
if (notifyClient)
730752
await socket.send(
731753
stringifyMessage<MessageType.Complete>(
@@ -749,7 +771,7 @@ export function makeServer<
749771
if (maybeExecArgsOrErrors) {
750772
if (areGraphQLErrors(maybeExecArgsOrErrors))
751773
return id in ctx.subscriptions
752-
? await emit.error(maybeExecArgsOrErrors)
774+
? await emit.error(maybeExecArgsOrErrors, message)
753775
: void 0;
754776
else if (Array.isArray(maybeExecArgsOrErrors))
755777
throw new Error(
@@ -772,7 +794,7 @@ export function makeServer<
772794
...args,
773795
schema:
774796
typeof schema === 'function'
775-
? await schema(ctx, message, args)
797+
? await schema(ctx, id, payload, args)
776798
: schema,
777799
};
778800
const validationErrors = (validate ?? graphqlValidate)(
@@ -781,7 +803,7 @@ export function makeServer<
781803
);
782804
if (validationErrors.length > 0)
783805
return id in ctx.subscriptions
784-
? await emit.error(validationErrors)
806+
? await emit.error(validationErrors, message)
785807
: void 0;
786808
}
787809

@@ -791,9 +813,10 @@ export function makeServer<
791813
);
792814
if (!operationAST)
793815
return id in ctx.subscriptions
794-
? await emit.error([
795-
new GraphQLError('Unable to identify operation'),
796-
])
816+
? await emit.error(
817+
[new GraphQLError('Unable to identify operation')],
818+
message,
819+
)
797820
: void 0;
798821

799822
// if `onSubscribe` didn't specify a rootValue, inject one
@@ -804,7 +827,7 @@ export function makeServer<
804827
if (!('contextValue' in execArgs))
805828
execArgs.contextValue =
806829
typeof context === 'function'
807-
? await context(ctx, message, execArgs)
830+
? await context(ctx, id, payload, execArgs)
808831
: context;
809832

810833
// the execution arguments have been prepared
@@ -820,7 +843,8 @@ export function makeServer<
820843

821844
const maybeResult = await onOperation?.(
822845
ctx,
823-
message.id,
846+
id,
847+
payload,
824848
execArgs,
825849
operationResult,
826850
);
@@ -836,41 +860,44 @@ export function makeServer<
836860
ctx.subscriptions[id] = operationResult;
837861
try {
838862
for await (const result of operationResult) {
839-
await emit.next(result, execArgs);
863+
await emit.next(result, message, execArgs);
840864
}
841865
} catch (err) {
842866
const originalError =
843867
err instanceof Error ? err : new Error(String(err));
844-
await emit.error([
845-
versionInfo.major >= 16
846-
? new GraphQLError(
847-
originalError.message,
848-
// @ts-ignore graphql@15 and less dont have the second arg as object (version is ensured by versionInfo.major check above)
849-
{ originalError },
850-
)
851-
: // versionInfo.major <= 15
852-
new GraphQLError(
853-
originalError.message,
854-
null,
855-
null,
856-
null,
857-
null,
858-
originalError,
859-
),
860-
]);
868+
await emit.error(
869+
[
870+
versionInfo.major >= 16
871+
? new GraphQLError(
872+
originalError.message,
873+
// @ts-ignore graphql@15 and less dont have the second arg as object (version is ensured by versionInfo.major check above)
874+
{ originalError },
875+
)
876+
: // versionInfo.major <= 15
877+
new GraphQLError(
878+
originalError.message,
879+
null,
880+
null,
881+
null,
882+
null,
883+
originalError,
884+
),
885+
],
886+
message,
887+
);
861888
}
862889
}
863890
} else {
864891
/** single emitted result */
865892
// if the client completed the subscription before the single result
866893
// became available, he effectively canceled it and no data should be sent
867894
if (id in ctx.subscriptions)
868-
await emit.next(operationResult, execArgs);
895+
await emit.next(operationResult, message, execArgs);
869896
}
870897

871898
// lack of subscription at this point indicates that the client
872899
// completed the subscription, he doesn't need to be reminded
873-
await emit.complete(id in ctx.subscriptions);
900+
await emit.complete(id in ctx.subscriptions, message);
874901
} finally {
875902
// whatever happens to the subscription, we finally want to get rid of the reservation
876903
delete ctx.subscriptions[id];

tests/server.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,8 @@ it('should use the schema resolved from a promise on subscribe', async ({
4545
const schema = new GraphQLSchema(schemaConfig);
4646

4747
const { url } = await startTServer({
48-
schema: (_, msg) => {
49-
expect(msg.id).toBe('1');
48+
schema: (_, id) => {
49+
expect(id).toBe('1');
5050
return Promise.resolve(schema);
5151
},
5252
execute: (args) => {
@@ -2138,7 +2138,7 @@ describe.concurrent('Disconnect/close', () => {
21382138
waitForComplete,
21392139
waitForClientClose,
21402140
} = await startTServer({
2141-
onOperation(_ctx, _msg, _args, result) {
2141+
onOperation(_ctx, _id, _msg, _args, result) {
21422142
const origReturn = (result as AsyncGenerator).return;
21432143
(result as AsyncGenerator).return = async (...args) => {
21442144
if (++i === 1) {

0 commit comments

Comments
 (0)