From 310571a52e8a1a032ae109572b54b0dbe55fc430 Mon Sep 17 00:00:00 2001 From: Iris Artin Date: Wed, 13 Sep 2023 17:01:14 -0400 Subject: [PATCH 1/4] Added lambda adapter --- grafast/grafserv/examples/example-lambda.mjs | 11 ++ grafast/grafserv/package.json | 1 + .../grafserv/src/servers/lambda/v1/index.ts | 128 ++++++++++++++++++ grafast/grafserv/src/utils.ts | 10 ++ grafast/website/grafserv/servers/lambda.md | 15 +- yarn.lock | 8 ++ 6 files changed, 170 insertions(+), 3 deletions(-) create mode 100644 grafast/grafserv/examples/example-lambda.mjs create mode 100644 grafast/grafserv/src/servers/lambda/v1/index.ts diff --git a/grafast/grafserv/examples/example-lambda.mjs b/grafast/grafserv/examples/example-lambda.mjs new file mode 100644 index 0000000000..aa21eb5a93 --- /dev/null +++ b/grafast/grafserv/examples/example-lambda.mjs @@ -0,0 +1,11 @@ +import { grafserv } from "grafserv/lambda/v1"; + +import preset from "./graphile.config.mjs"; +import schema from "./schema.mjs"; + +// Create a Grafserv instance +const serv = grafserv({ schema, preset }); + +// Let lambda call into its handler +export const handler = serv.createHandler() + diff --git a/grafast/grafserv/package.json b/grafast/grafserv/package.json index 86217e23e1..8a61c25dca 100644 --- a/grafast/grafserv/package.json +++ b/grafast/grafserv/package.json @@ -96,6 +96,7 @@ } }, "devDependencies": { + "@types/aws-lambda": "^8.10.123", "@types/express": "^4.17.17", "@types/koa": "^2.13.8", "@types/koa-bodyparser": "^4.3.10", diff --git a/grafast/grafserv/src/servers/lambda/v1/index.ts b/grafast/grafserv/src/servers/lambda/v1/index.ts new file mode 100644 index 0000000000..4fcec82f53 --- /dev/null +++ b/grafast/grafserv/src/servers/lambda/v1/index.ts @@ -0,0 +1,128 @@ +import type { + APIGatewayEvent, + APIGatewayProxyEvent, + APIGatewayProxyResult, + Context as LambdaContext, +} from "aws-lambda"; + +import { GrafservBase } from "../../../core/base.js"; +import type { + GrafservConfig, + RequestDigest, + Result, +} from "../../../interfaces.js"; +import { processHeaders } from "../../../utils.js"; + +declare global { + // eslint-disable-next-line @typescript-eslint/no-namespace + namespace Grafast { + interface RequestContext { + lambdav1: { event: APIGatewayProxyEvent; context: LambdaContext }; + } + } +} + +export class LambdaGrafserv extends GrafservBase { + protected lambdaRequestToGrafserv( + event: APIGatewayEvent, + context: LambdaContext, + ): RequestDigest { + const version = event.requestContext.protocol.match( + /^HTTP\/(?[0-9]+)\.(?[0-9]+)$/, + ); + + return { + httpVersionMajor: parseInt(version?.groups?.major ?? "1"), + httpVersionMinor: parseInt(version?.groups?.minor ?? "0"), + isSecure: false, // Because we don't trust X-Forwarded-Proto + method: event.httpMethod, + path: event.requestContext.path, + headers: processHeaders(event.multiValueHeaders), + getQueryParams() { + return Object.fromEntries( + Object.entries(event.queryStringParameters ?? {}).filter( + ([_k, v]) => v !== undefined, + ), + ) as Record; + }, + getBody() { + return { + type: "text", + text: event.body ?? "", + }; + }, + requestContext: { + lambdav1: { event, context }, + }, + preferJSON: true, + }; + } + + protected grafservResponseToLambda(response: Result | null) { + if (response === null) { + return { + statusCode: 404, + body: "¯\\_(ツ)_/¯", + }; + } + + switch (response.type) { + case "error": { + const { statusCode, headers, error } = response; + return { + statusCode, + headers: { ...headers, "Content-Type": "text/plain" }, + body: error.message, + }; + } + + case "buffer": { + const { statusCode, headers, buffer } = response; + return { statusCode, headers, body: buffer.toString() }; + } + + case "json": { + const { statusCode, headers, json } = response; + return { statusCode, headers, body: JSON.stringify(json) }; + } + + default: { + console.log("Unhandled:"); + console.dir(response); + return { + statusCode: 503, + headers: { "Content-Type": "text/plain" }, + body: "Server hasn't implemented this yet", + }; + } + } + } + + createHandler() { + // eslint-disable-next-line @typescript-eslint/no-this-alias + return async ( + event: APIGatewayEvent, + context: LambdaContext, + ): Promise => { + return this.grafservResponseToLambda( + await this.processLambdaRequest( + event, + context, + this.lambdaRequestToGrafserv(event, context), + ), + ); + }; + } + + protected processLambdaRequest( + _event: APIGatewayEvent, + _context: LambdaContext, + request: RequestDigest, + ) { + return this.processRequest(request); + } +} + +export function grafserv(config: GrafservConfig) { + return new LambdaGrafserv(config); +} diff --git a/grafast/grafserv/src/utils.ts b/grafast/grafserv/src/utils.ts index 289ce831b0..857929cdae 100644 --- a/grafast/grafserv/src/utils.ts +++ b/grafast/grafserv/src/utils.ts @@ -292,3 +292,13 @@ export function parseGraphQLJSONBody( extensions, }; } + +export async function concatBufferIterator( + bufferIterator: AsyncGenerator, +) { + const buffers = []; + for await (const buffer of bufferIterator) { + buffers.push(buffer); + } + return Buffer.concat(buffers); +} diff --git a/grafast/website/grafserv/servers/lambda.md b/grafast/website/grafserv/servers/lambda.md index 70b1417ec0..7244530032 100644 --- a/grafast/website/grafserv/servers/lambda.md +++ b/grafast/website/grafserv/servers/lambda.md @@ -1,9 +1,18 @@ # Lambda -**TODO: actually implement this!** +Grafserv supports the following AWS lambda configurations: + +## AWS API Gateway v2 + +To deploy Grafserv in API Gateway v2: + +- Create an Node 18.x lambda following the instructions at https://docs.aws.amazon.com/lambda/latest/dg/lambda-nodejs.html +- Add `grafserv` as a dependency using your node package manager of choice +- Replace your lambda's handler implementation with the code below +- Deploy your lambda as a zip package following the instructions at https://docs.aws.amazon.com/lambda/latest/dg/nodejs-package.html#nodejs-package-create-dependencies ```js -import { grafserv } from "grafserv/lambda"; +import { grafserv } from "grafserv/lambda/v1"; import preset from "./graphile.config.mjs"; import schema from "./schema.mjs"; @@ -11,5 +20,5 @@ import schema from "./schema.mjs"; const serv = grafserv({ schema, preset }); // Export a lambda handler for GraphQL -export const handler = serv.createGraphQLHandler(); +export const handler = serv.createHandler(); ``` diff --git a/yarn.lock b/yarn.lock index e71caaa23c..59a64de964 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5105,6 +5105,13 @@ __metadata: languageName: node linkType: hard +"@types/aws-lambda@npm:^8.10.123": + version: 8.10.123 + resolution: "@types/aws-lambda@npm:8.10.123" + checksum: 3336788a2b0fbdca0ce1b643ab5c35219e939ef0e0fa474029efe7cd618e5e8c3dc68180bd8ac3eaeb73b234d4cd01c2a30b7601e6eb0ecfc863b5142966c0b0 + languageName: node + linkType: hard + "@types/babel__core@npm:^7.1.14": version: 7.20.1 resolution: "@types/babel__core@npm:7.20.1" @@ -11789,6 +11796,7 @@ __metadata: resolution: "grafserv@workspace:grafast/grafserv" dependencies: "@graphile/lru": "workspace:^" + "@types/aws-lambda": "npm:^8.10.123" "@types/express": "npm:^4.17.17" "@types/koa": "npm:^2.13.8" "@types/koa-bodyparser": "npm:^4.3.10" From d1f6a754448d24189a3600a6c5b43d3d1d30203a Mon Sep 17 00:00:00 2001 From: Benjie Date: Thu, 5 Oct 2023 10:20:41 +0100 Subject: [PATCH 2/4] Apply suggestions from code review --- grafast/grafserv/src/servers/lambda/v1/index.ts | 6 ++++-- grafast/website/grafserv/servers/lambda.md | 2 ++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/grafast/grafserv/src/servers/lambda/v1/index.ts b/grafast/grafserv/src/servers/lambda/v1/index.ts index 4fcec82f53..8cc9307f8b 100644 --- a/grafast/grafserv/src/servers/lambda/v1/index.ts +++ b/grafast/grafserv/src/servers/lambda/v1/index.ts @@ -22,6 +22,7 @@ declare global { } } +/** @experimental */ export class LambdaGrafserv extends GrafservBase { protected lambdaRequestToGrafserv( event: APIGatewayEvent, @@ -78,7 +79,7 @@ export class LambdaGrafserv extends GrafservBase { case "buffer": { const { statusCode, headers, buffer } = response; - return { statusCode, headers, body: buffer.toString() }; + return { statusCode, headers, body: buffer.toString("utf8") }; } case "json": { @@ -90,7 +91,7 @@ export class LambdaGrafserv extends GrafservBase { console.log("Unhandled:"); console.dir(response); return { - statusCode: 503, + statusCode: 501, headers: { "Content-Type": "text/plain" }, body: "Server hasn't implemented this yet", }; @@ -123,6 +124,7 @@ export class LambdaGrafserv extends GrafservBase { } } +/** @experimental */ export function grafserv(config: GrafservConfig) { return new LambdaGrafserv(config); } diff --git a/grafast/website/grafserv/servers/lambda.md b/grafast/website/grafserv/servers/lambda.md index 7244530032..fa52285b34 100644 --- a/grafast/website/grafserv/servers/lambda.md +++ b/grafast/website/grafserv/servers/lambda.md @@ -1,5 +1,7 @@ # Lambda +**THIS INTEGRATION IS EXPERIMENTAL**. PRs improving it are welcome. + Grafserv supports the following AWS lambda configurations: ## AWS API Gateway v2 From a38e650d67d6c7ff0cf5b853377622090ede3a50 Mon Sep 17 00:00:00 2001 From: Iris Artin Date: Thu, 5 Oct 2023 10:57:58 -0400 Subject: [PATCH 3/4] docs(changeset): Added AWS lambda adapter for grafserv --- .changeset/twelve-apes-try.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/twelve-apes-try.md diff --git a/.changeset/twelve-apes-try.md b/.changeset/twelve-apes-try.md new file mode 100644 index 0000000000..2be31d16ca --- /dev/null +++ b/.changeset/twelve-apes-try.md @@ -0,0 +1,5 @@ +--- +"grafserv": minor +--- + +Added AWS lambda adapter for grafserv From 82bf624dbae3949682c9497c746a431e12c0384b Mon Sep 17 00:00:00 2001 From: Benjie Date: Thu, 5 Oct 2023 17:39:01 +0100 Subject: [PATCH 4/4] changesets: minor -> patch --- .changeset/twelve-apes-try.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/twelve-apes-try.md b/.changeset/twelve-apes-try.md index 2be31d16ca..5a332feeb5 100644 --- a/.changeset/twelve-apes-try.md +++ b/.changeset/twelve-apes-try.md @@ -1,5 +1,5 @@ --- -"grafserv": minor +"grafserv": patch --- Added AWS lambda adapter for grafserv