1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
|
/*** UTILITY ------------------------------------------ ***/
import { runHttpQuery } from "./common.ts";
import type {
GQLOptions,
GQLRequest,
GraphQLParams
} from "./utility/types.ts";
/*** EXPORT ------------------------------------------- ***/
/**
* A handler that accepts a `Request`-shaped object and returns a `Response`.
*
* Returned by {@link GraphQLHTTP}; plug it into `Deno.serve`, `serve()`, or
* any router that speaks the Fetch API.
*/
export type GraphQLHandler<Req extends GQLRequest = GQLRequest> = (request: Req) => Promise<Response>;
/**
* Builds an HTTP handler that serves a GraphQL schema.
*
* Content negotiation:
* - `GET` with `Accept: text/html` and `graphiql: true` renders the Playground.
* - `GET` with `?query=...` executes the query.
* - `POST`/`PUT`/`PATCH` read JSON from the body.
* - Anything else returns 405; unacceptable `Accept` headers return 406.
*
* @param options - Schema, optional context builder, custom headers, and
* Playground toggles. See {@link GQLOptions}.
* @returns A {@link GraphQLHandler} ready to be mounted on any Fetch-compatible server.
*
* @example
* ```ts
* import { executeSchema, GraphQLHTTP, gql } from "@eol/gq";
*
* const schema = executeSchema({
* typeDefs: gql`type Query { hello: String }`,
* resolvers: { Query: { hello: () => "world" } }
* });
*
* Deno.serve(GraphQLHTTP({ schema, graphiql: true }));
* ```
*/
export function GraphQLHTTP<
Req extends GQLRequest = GQLRequest,
Ctx extends { request: Req } = { request: Req }>({
playgroundOptions = {},
headers = {},
...options
}: GQLOptions<Ctx, Req>): GraphQLHandler<Req> {
return async (request: Req) => {
const accept = request.headers.get("Accept") || "";
const typeList = ["application/json", "text/html", "text/plain", "*/*"]
.map(contentType => ({
contentType,
index: accept.indexOf(contentType)
}))
.filter(({ index }) => index >= 0)
.sort((a, b) => a.index - b.index)
.map(({ contentType }) => contentType);
if (accept && !typeList.length) {
return new Response("Not Acceptable", {
headers: new Headers(headers),
status: 406
});
} else if (!["GET", "PUT", "POST", "PATCH"].includes(request.method)) {
return new Response("Method Not Allowed", {
headers: new Headers(headers),
status: 405
});
}
let params: Promise<GraphQLParams>;
if (request.method === "GET") {
const urlQuery = request.url.substring(request.url.indexOf("?"));
const queryParams = new URLSearchParams(urlQuery);
if (options.graphiql && typeList[0] === "text/html" && !queryParams.has("raw")) {
const { renderPlaygroundPage } = await import("./graphiql/render.ts");
const playground = renderPlaygroundPage({
...playgroundOptions,
endpoint: "/graphql"
});
return new Response(playground, {
headers: new Headers({
"Content-Type": "text/html",
...headers
})
});
} else if (typeList.length === 1 && typeList[0] === "text/html") {
return new Response("Not Acceptable", {
headers: new Headers(headers),
status: 406
});
} else if (queryParams.has("query")) {
params = Promise.resolve({ query: queryParams.get("query") } as GraphQLParams);
} else {
params = Promise.reject(new Error("No query given!"));
}
} else if (typeList.length === 1 && typeList[0] === "text/html") {
return new Response("Not Acceptable", {
headers: new Headers(headers),
status: 406
});
} else {
params = request.json();
}
try {
const result = await runHttpQuery<Req, Ctx>(await params, options, request);
let contentType = "text/plain";
if (!typeList.length || typeList.includes("application/json") || typeList.includes("*/*"))
contentType = "application/json";
return new Response(JSON.stringify(result, null, 2), {
headers: new Headers({
"Content-Type": contentType,
...headers
}),
status: 200
});
} catch(e) {
console.error(e);
return new Response(
"Malformed Request " + (request.method === "GET" ? "Query" : "Body"), {
headers: new Headers(headers),
status: 400
}
);
}
}
}
|