aboutsummaryrefslogtreecommitdiff
path: root/source/http.ts
diff options
context:
space:
mode:
authornetop://ウィビ <paul@webb.page>2026-04-24 07:43:33 -0700
committernetop://ウィビ <paul@webb.page>2026-04-24 07:43:33 -0700
commit4c6194c4c2b5506f6d482347b0c13033ef17b5c7 (patch)
tree310b315b23487b9a44da94cd21a970f6cc95c831 /source/http.ts
downloadgq-4c6194c4c2b5506f6d482347b0c13033ef17b5c7.tar.gz
gq-4c6194c4c2b5506f6d482347b0c13033ef17b5c7.zip
initial commit
Diffstat (limited to '')
-rwxr-xr-xsource/http.ts144
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
+ }
+ );
+ }
+ }
+}