Skip to content

Next.js adaptor #3067

Description

@benjie

Hacked this together quickly:

import { PostGraphileAmberPreset } from "postgraphile/presets/amber";
import { makePgService } from "postgraphile/adaptors/pg";
import { readFileSync } from "node:fs";

const { version } = JSON.parse(readFileSync("node_modules/ruru/package.json"));

/** @type {GraphileConfig.Preset} */
const preset = {
  extends: [PostGraphileAmberPreset],
  pgServices: [makePgService({ connectionString: "postgres:///todo" })],
  grafserv: {
    watch: true,
    graphqlPath: "/api/graphql",
    eventStreamPath: "/api/graphql/stream",
    graphiqlPath: "/api/graphiql",
    graphiqlStaticPath: `https://unpkg.com/ruru@${version}/static/`,
  },
};

export default preset;
import type { NextApiRequest, NextApiResponse } from "next";
import { Readable } from "node:stream";

import {
  convertHandlerResultToResult,
  GrafservBase,
  normalizeRequest,
  processHeaders,
} from "grafserv";
import type {
  EventStreamHandlerResult,
  GrafservBodyBuffer,
  GrafservBodyJSON,
  GrafservConfig,
  RequestDigest,
  Result,
} from "grafserv";

declare global {
  namespace Grafast {
    interface RequestContext {
      nextv16: {
        req: NextApiRequest;
        res: NextApiResponse;
      };
    }
  }
}

function getDigest(req: NextApiRequest, res: NextApiResponse): RequestDigest {
  return {
    httpVersionMajor: req.httpVersionMajor,
    httpVersionMinor: req.httpVersionMinor,
    isSecure: "encrypted" in req.socket ? !!req.socket.encrypted : false,
    method: req.method!,
    path: req.url!,
    headers: processHeaders(req.headers),
    getQueryParams() {
      return req.query as Record<string, string | string[]>;
    },
    async getBody() {
      const body = req.body;
      if (Buffer.isBuffer(body)) {
        return {
          type: "buffer",
          buffer: body,
        } as GrafservBodyBuffer;
      } else if (typeof body === "object" && body !== null) {
        return {
          type: "json",
          json: body,
        } as GrafservBodyJSON;
      } else {
        throw new Error("Failed to retrieve body from next");
      }
    },
    requestContext: {
      nextv16: {
        req,
        res,
      },
    },
  };
}

export class NextGrafserv extends GrafservBase {
  constructor(config: GrafservConfig) {
    super(config);
  }

  public async send(
    req: NextApiRequest,
    res: NextApiResponse,
    result: Result | null,
  ) {
    if (result === null) {
      return res.status(404).send("¯\\_(ツ)_/¯");
    }

    switch (result.type) {
      case "error": {
        const { statusCode, headers } = result;
        res
          .setHeaders(new Map(Object.entries(headers)))
          .status(statusCode)
          .json({ errors: [result.error] });
        return;
      }
      case "buffer": {
        const { statusCode, headers, buffer } = result;
        res
          .setHeaders(new Map(Object.entries(headers)))
          .status(statusCode)
          .send(buffer);
        return;
      }
      case "json": {
        const { statusCode, headers, json } = result;
        res
          .setHeaders(new Map(Object.entries(headers)))
          .status(statusCode)
          .json(json);
        return;
      }
      case "noContent": {
        const { statusCode, headers } = result;
        res
          .setHeaders(new Map(Object.entries(headers)))
          .status(statusCode)
          .send("");
        return;
      }
      case "bufferStream": {
        const { statusCode, headers, lowLatency, bufferIterator } = result;
        res
          .setHeaders(new Map(Object.entries(headers)))
          .status(statusCode)
          .send(Readable.from(bufferIterator));
        return;
      }
      default: {
        const never: never = result;
        console.log("Unhandled:");
        console.dir(never);
        res
          .setHeaders(
            new Map(
              Object.entries({
                "Content-Type": "text/plain",
              }),
            ),
          )
          .status(501)
          .send("Server hasn't implemented this yet");
        return;
      }
    }
  }

  public createGraphQLHandler() {
    return async (req: NextApiRequest, res: NextApiResponse) => {
      const digest = getDigest(req, res);

      const handlerResult = await this.graphqlHandler(
        normalizeRequest(digest),
        this.graphiqlHandler,
      );
      const result = await convertHandlerResultToResult(handlerResult);
      return this.send(req, res, result);
    };
  }

  public createGraphiQLHandler() {
    return async (req: NextApiRequest, res: NextApiResponse) => {
      const digest = getDigest(req, res);

      const handlerResult = await this.graphiqlHandler(
        normalizeRequest(digest),
      );
      const result = await convertHandlerResultToResult(handlerResult);
      return this.send(req, res, result);
    };
  }

  public createGraphQLEventStreamHandler() {
    return async (req: NextApiRequest, res: NextApiResponse) => {
      const digest = getDigest(req, res);

      const handlerResult: EventStreamHandlerResult = {
        type: "event-stream",
        request: normalizeRequest(digest),
        dynamicOptions: this.dynamicOptions,
        payload: this.makeStream(),
        statusCode: 200,
      };
      const result = await convertHandlerResultToResult(handlerResult);
      return this.send(req, res, result);
    };
  }
}

export function grafserv(config: GrafservConfig) {
  return new NextGrafserv(config);
}

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions