Skip to content

Commit 79c44e0

Browse files
committed
feat: add validation for message data
1 parent 56550ec commit 79c44e0

File tree

6 files changed

+255
-6
lines changed

6 files changed

+255
-6
lines changed

clients.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import {
1010
SubscribeMessage,
1111
} from "./types.ts";
1212
import { isString } from "./deps.ts";
13-
import { resolveData } from "./resolve.ts";
13+
import { parseMessage } from "./parse.ts";
1414

1515
export interface GraphQLWebSocketEventMap {
1616
next: MessageEvent<NextMessage>;
@@ -95,7 +95,7 @@ export class ClientImpl extends WebSocket implements Client {
9595
): void {
9696
if (["next"].includes(type)) {
9797
this.#ws.addEventListener("message", (ev) => {
98-
const [data, error] = resolveData(ev.data);
98+
const [data, error] = parseMessage(ev.data);
9999

100100
if (!data) {
101101
return this.close(CloseCode.BadRequest, error.message);

deps.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ export {
55
isString,
66
isUndefined,
77
} from "https://deno.land/x/[email protected]/mod.ts";
8+
import { isObject } from "https://deno.land/x/[email protected]/mod.ts";
89
export {
910
JSON,
1011
type json,
@@ -55,3 +56,9 @@ export function tryCatchSync<R, E>(
5556
return [, er];
5657
}
5758
}
59+
60+
export function isPlainObject(
61+
value: unknown,
62+
): value is Record<PropertyKey, unknown> {
63+
return isObject(value) && value.constructor === Object;
64+
}

dev_deps.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
export * from "https://deno.land/[email protected]/testing/bdd.ts";
2+
import {
3+
defineExpect,
4+
jestMatcherMap,
5+
jestModifierMap,
6+
} from "https://deno.land/x/[email protected]/mod.ts";
7+
8+
export const expect = defineExpect({
9+
matcherMap: {
10+
...jestMatcherMap,
11+
toError: (
12+
actual: unknown,
13+
// deno-lint-ignore ban-types
14+
error: Function,
15+
message?: string,
16+
) => {
17+
if (!(actual instanceof Error)) {
18+
return {
19+
pass: false,
20+
expected: "Error Object",
21+
};
22+
}
23+
24+
if (!(actual instanceof error)) {
25+
return {
26+
pass: false,
27+
expected: `${error.name} Object`,
28+
};
29+
}
30+
31+
if (message) {
32+
return {
33+
pass: actual.message === message,
34+
expected: message,
35+
resultActual: actual.message,
36+
};
37+
}
38+
39+
return {
40+
pass: true,
41+
expected: error,
42+
};
43+
},
44+
},
45+
modifierMap: jestModifierMap,
46+
});

handler.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,9 @@ import {
1818
GRAPHQL_TRANSPORT_WS_PROTOCOL,
1919
MessageType,
2020
} from "./constants.ts";
21-
import { resolveData } from "./resolve.ts";
2221
import { GraphQLExecutionArgs } from "./types.ts";
2322
import { validateWebSocketRequest } from "./validates.ts";
23+
import { parseMessage } from "./parse.ts";
2424

2525
/**
2626
* @throws `AggregateError` - When GraphQL schema validation error has occurred.
@@ -61,7 +61,6 @@ export default function createHandler(
6161
}
6262

6363
register(data.socket, params);
64-
console.log(data.response);
6564
return data.response;
6665
};
6766
}
@@ -82,7 +81,7 @@ function register(
8281
});
8382

8483
socket.addEventListener("message", async ({ data }) => {
85-
const [message, error] = resolveData(data);
84+
const [message, error] = parseMessage(data);
8685

8786
if (!message) {
8887
return socket.close(

parse.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { isString, JSON } from "./deps.ts";
2+
import { Message } from "./types.ts";
3+
import { validateMessage } from "./validates.ts";
4+
5+
export function parseMessage(
6+
message: unknown,
7+
): [data: Message] | [data: undefined, error: SyntaxError | TypeError] {
8+
if (!isString(message)) {
9+
return [, TypeError("Invalid data type. Message must be string.")];
10+
}
11+
12+
const [data, error] = JSON.parse(message);
13+
14+
if (error) {
15+
return [, error];
16+
}
17+
18+
return validateMessage(data);
19+
}

validates.ts

Lines changed: 179 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,20 @@
1-
import { createHttpError, HttpError, isNull, Status } from "./deps.ts";
1+
import {
2+
createHttpError,
3+
has,
4+
HttpError,
5+
isNull,
6+
isPlainObject,
7+
isString,
8+
Status,
9+
} from "./deps.ts";
10+
import { MessageType } from "./constants.ts";
11+
import {
12+
CompleteMessage,
13+
GraphQLParameters,
14+
Message,
15+
NextMessage,
16+
SubscribeMessage,
17+
} from "./types.ts";
218

319
export function validateWebSocketRequest(req: Request): [result: true] | [
420
result: false,
@@ -79,3 +95,165 @@ export function validateWebSocketRequest(req: Request): [result: true] | [
7995

8096
return [true];
8197
}
98+
99+
export function validateMessage(
100+
value: unknown,
101+
): [data: Message] | [data: undefined, error: Error] {
102+
if (!isPlainObject(value)) {
103+
return [
104+
,
105+
Error(
106+
`Invalid data type. Must be plain object.`,
107+
),
108+
];
109+
}
110+
111+
if (!has(value, "type")) {
112+
return [, Error(`Missing field. Must include "type" field.`)];
113+
}
114+
if (!isString(value.type)) {
115+
return [, Error(`Invalid field. "type" field of value must be string.`)];
116+
}
117+
118+
switch (value.type) {
119+
case MessageType.ConnectionInit:
120+
case MessageType.ConnectionAck:
121+
case MessageType.Ping:
122+
case MessageType.Pong: {
123+
if (has(value, "payload") && !isPlainObject(value.payload)) {
124+
return [, Error(`Invalid field. "payload" must be plain object.`)];
125+
}
126+
127+
return [value as Message];
128+
}
129+
130+
case MessageType.Subscribe: {
131+
if (!has(value, "id")) {
132+
return [, Error(`Missing field. "id"`)];
133+
}
134+
if (!isString(value.id)) {
135+
return [
136+
,
137+
Error(
138+
`Invalid field. "id" must be string.`,
139+
),
140+
];
141+
}
142+
if (!has(value, "payload")) {
143+
return [, Error(`Missing field. "payload"`)];
144+
}
145+
146+
const graphqlParametersResult = validateGraphQLParameters(value.payload);
147+
148+
if (!graphqlParametersResult[0]) {
149+
return graphqlParametersResult;
150+
}
151+
152+
return [
153+
{ ...value, payload: graphqlParametersResult[0] } as SubscribeMessage,
154+
];
155+
}
156+
157+
case MessageType.Next: {
158+
if (!has(value, "id")) {
159+
return [, Error(`Missing property. "id"`)];
160+
}
161+
if (!has(value, "payload")) {
162+
return [, Error(`Missing property. "payload"`)];
163+
}
164+
return [value as NextMessage];
165+
}
166+
167+
case MessageType.Complete: {
168+
if (!has(value, "id")) {
169+
return [, Error(`Missing property. "id"`)];
170+
}
171+
172+
return [value as CompleteMessage];
173+
}
174+
175+
default: {
176+
return [
177+
,
178+
Error(
179+
`Invalid field. "type" field of "${value.type}" is not supported.`,
180+
),
181+
];
182+
}
183+
}
184+
}
185+
186+
export function validateGraphQLParameters(
187+
value: unknown,
188+
): [data: GraphQLParameters] | [data: undefined, error: Error] {
189+
if (!isPlainObject(value)) {
190+
return [
191+
,
192+
Error(
193+
`Invalid field. "payload" must be plain object.`,
194+
),
195+
];
196+
}
197+
198+
if (!has(value, "query")) {
199+
return [
200+
,
201+
Error(
202+
`Missing field. "query"`,
203+
),
204+
];
205+
}
206+
207+
if (!isString(value.query)) {
208+
return [
209+
,
210+
Error(
211+
`Invalid field. "query" must be string.`,
212+
),
213+
];
214+
}
215+
216+
if (
217+
has(value, "variables") &&
218+
(!isNull(value.variables) && !isPlainObject(value.variables))
219+
) {
220+
return [
221+
,
222+
Error(
223+
`Invalid field. "variables" must be plain object or null`,
224+
),
225+
];
226+
}
227+
if (
228+
has(value, "operationName") &&
229+
(!isNull(value.operationName) && !isString(value.operationName))
230+
) {
231+
return [
232+
,
233+
Error(
234+
`Invalid field. "operationName" must be string or null.`,
235+
),
236+
];
237+
}
238+
if (
239+
has(value, "extensions") &&
240+
(!isNull(value.extensions) && !isPlainObject(value.extensions))
241+
) {
242+
return [
243+
,
244+
Error(
245+
`Invalid field. "extensions" must be plain object or null`,
246+
),
247+
];
248+
}
249+
250+
const { query, ...rest } = value;
251+
252+
return [{
253+
operationName: null,
254+
variableValues: null,
255+
extensions: null,
256+
query,
257+
...rest,
258+
}];
259+
}

0 commit comments

Comments
 (0)