/*** 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 = (request: Req) => Promise; /** * 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({ * resolvers: { Query: { hello: () => "world" } }, * typeDefs: gql`type Query { hello: String }` * }); * * Deno.serve(GraphQLHTTP({ graphiql: true, schema })); * ``` */ export function GraphQLHTTP< Req extends GQLRequest = GQLRequest, Ctx extends { request: Req } = { request: Req }>({ playgroundOptions = {}, headers = {}, ...options }: GQLOptions): GraphQLHandler { 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; 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(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 } ); } } }