diff options
| author | netop://ウィビ <paul@webb.page> | 2026-04-24 07:43:33 -0700 |
|---|---|---|
| committer | netop://ウィビ <paul@webb.page> | 2026-04-24 07:43:33 -0700 |
| commit | 4c6194c4c2b5506f6d482347b0c13033ef17b5c7 (patch) | |
| tree | 310b315b23487b9a44da94cd21a970f6cc95c831 /source/http.ts | |
| download | gq-4c6194c4c2b5506f6d482347b0c13033ef17b5c7.tar.gz gq-4c6194c4c2b5506f6d482347b0c13033ef17b5c7.zip | |
initial commit
Diffstat (limited to '')
| -rwxr-xr-x | source/http.ts | 144 |
1 files changed, 144 insertions, 0 deletions
diff --git a/source/http.ts b/source/http.ts new file mode 100755 index 0000000..7d867a5 --- /dev/null +++ b/source/http.ts @@ -0,0 +1,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 + } + ); + } + } +} |