diff --git a/grafast/grafserv/__tests__/exampleServer.ts b/grafast/grafserv/__tests__/exampleServer.ts index 0be92b1199..a0dc1e595b 100644 --- a/grafast/grafserv/__tests__/exampleServer.ts +++ b/grafast/grafserv/__tests__/exampleServer.ts @@ -1,10 +1,12 @@ -import { createServer } from "node:http"; +import { createServer, Server } from "node:http"; import type { AddressInfo } from "node:net"; import { constant, error, makeGrafastSchema } from "grafast"; import { resolvePreset } from "graphile-config"; -import { grafserv } from "../src/servers/node/index.js"; +import { grafserv as grafservNode } from "../src/servers/node/index.js"; +import { grafserv as grafservWhatwg } from "../src/servers/whatwg-node-server"; +import { GrafservBase } from "../src/index.js"; export async function makeExampleServer( preset: GraphileConfig.Preset = { @@ -14,6 +16,7 @@ export async function makeExampleServer( dangerouslyAllowAllCORSRequests: true, }, }, + type?: 'node' | 'whatwg' ) { const resolvedPreset = resolvePreset(preset); const schema = makeGrafastSchema({ @@ -35,9 +38,22 @@ export async function makeExampleServer( }, }); - const serv = grafserv({ schema, preset }); - const server = createServer(); - serv.addTo(server); + let serv: GrafservBase + let server: ReturnType + switch (type) { + case 'whatwg': + const servWhatwg = grafservWhatwg({schema, preset}) + server = createServer(servWhatwg.createHandler()); + serv = servWhatwg + break; + case 'node': + default: + const servNode = grafservNode({ schema, preset }); + server = createServer(); + servNode.addTo(server); + serv = servNode + break; + } const promise = new Promise((resolve, reject) => { server.on("listening", () => { server.off("error", reject); diff --git a/grafast/grafserv/package.json b/grafast/grafserv/package.json index 4c7cc5d14d..62c9d45c89 100644 --- a/grafast/grafserv/package.json +++ b/grafast/grafserv/package.json @@ -90,6 +90,7 @@ }, "peerDependencies": { "@envelop/core": "^5.0.0", + "@whatwg-node/server": "^0.9.64", "grafast": "workspace:^", "graphile-config": "workspace:^", "graphql": "^16.1.0-experimental-stream-defer.6", @@ -100,6 +101,9 @@ "@envelop/core": { "optional": true }, + "@whatwg-node/server": { + "optional": true + }, "h3": { "optional": true }, @@ -115,6 +119,7 @@ "@types/koa": "^2.13.8", "@types/koa-bodyparser": "^4.3.10", "@whatwg-node/fetch": "^0.9.10", + "@whatwg-node/server": "^0.9.64", "express": "^4.20.0", "fastify": "^4.22.1", "grafast": "workspace:^", diff --git a/grafast/grafserv/src/servers/whatwg-node-server/index.ts b/grafast/grafserv/src/servers/whatwg-node-server/index.ts new file mode 100644 index 0000000000..0355eec36c --- /dev/null +++ b/grafast/grafserv/src/servers/whatwg-node-server/index.ts @@ -0,0 +1,154 @@ +import { createServerAdapter } from '@whatwg-node/server' + +import { GrafservBase } from "../../core/base.js"; +import type { + GrafservBody, + GrafservConfig, + RequestDigest, + Result, +} from "../../interfaces.js"; + +import { OptionsFromConfig } from '../../options.js'; +import { httpError } from '../../utils.js'; + +export function getBodyFromRequest( + req: Request /* IncomingMessage */, + maxLength: number, +): Promise { + return new Promise(async (resolve, reject) => { + const chunks: Buffer[] = []; + let len = 0; + const handleDataCb = (chunk: Uint8Array) => { + chunks.push(Buffer.from(chunk)); + len += chunk.length; + if (len > maxLength) { + reject(httpError(413, "Too much data")); + } + }; + const doneCb = () => { + resolve({ type: "buffer", buffer: Buffer.concat(chunks) }); + }; + const reader = req.body?.getReader() + if (!reader) { + return doneCb() + } + while (true) { + const {done, value} = await reader?.read() + if (value) { + handleDataCb(value) + } + if (done) { + return doneCb() + } + } + }); +} + +declare global { + // eslint-disable-next-line @typescript-eslint/no-namespace + namespace Grafast { + interface RequestContext { + whatwg: { + version:string + request: Request + } + } + } +} + +/** @experimental */ +export class WhatwgGrafserv extends GrafservBase { + protected whatwgRequestToGrafserv( + dynamicOptions: OptionsFromConfig, + request: Request + ): RequestDigest { + const url = new URL(request.url); + return { + httpVersionMajor: 1, + httpVersionMinor: 1, + isSecure: url.protocol === 'https:', + method: request.method, + path: url.pathname, + headers: this.processHeaders(request.headers), + getQueryParams() { + return Object.fromEntries(url.searchParams.entries()) as Record; + }, + async getBody() { + return getBodyFromRequest(request, dynamicOptions.maxRequestLength) + }, + requestContext: { + whatwg: { + version:'whatwgv1', + request + } + }, + preferJSON: true, + }; + } + + protected processHeaders(headers: Headers): Record { + const headerDigest: Record = Object.create(null); + headers.forEach((v,k)=> { + headerDigest[k]= v + }) + return headerDigest + } + + protected grafservResponseToWhatwg(response: Result | null): Response { + if (response === null) { + return new Response("¯\\_(ツ)_/¯", {status: 404, headers: new Headers({"Content-Type": "text/plain"})}) + } + + switch (response.type) { + case "error": { + const { statusCode, headers, error } = response; + const respHeaders = new Headers(headers) + respHeaders.append("Content-Type", "text/plain") + return new Response(error.message, {status: statusCode, headers:respHeaders}) + } + + case "buffer": { + const { statusCode, headers, buffer } = response; + const respHeaders = new Headers(headers) + return new Response(buffer, {status: statusCode, headers:respHeaders}) + } + + case "json": { + const { statusCode, headers, json } = response; + const respHeaders = new Headers(headers) + return new Response(JSON.stringify(json), {status: statusCode, headers:respHeaders}) + } + + default: { + console.log("Unhandled:"); + console.dir(response); + return new Response("Server hasn't implemented this yet", {status: 501, headers: new Headers({"Content-Type": "text/plain"})}) + } + } + } + + createHandler() { + // eslint-disable-next-line @typescript-eslint/no-this-alias + return createServerAdapter(async (request: Request): Promise => { + const dynamicOptions = this.dynamicOptions; + return this.grafservResponseToWhatwg( + await this.processWhatwgRequest( + request, + this.whatwgRequestToGrafserv(dynamicOptions, request), + ), + ); + }) + } + + protected processWhatwgRequest( + _request: Request, + request: RequestDigest, + ) { + return this.processRequest(request); + } +} + +/** @experimental */ +export function grafserv(config: GrafservConfig) { + return new WhatwgGrafserv(config); +}