Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/chilled-teachers-bathe.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@as-integrations/aws-lambda': major
---

Implement strictly type request handlers, custom handlers, and expose middleware functionality
267 changes: 263 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# `@as-integrations/aws-lambda`

## Getting started: Lambda middleware
## Getting started

Apollo Server runs as a part of your Lambda handler, processing GraphQL requests. This package allows you to easily integrate Apollo Server with AWS Lambda. This integration is compatible with Lambda's API Gateway V1 (REST) and V2 (HTTP). It doesn't currently claim support for any other flavors of Lambda, though PRs are welcome!

Expand All @@ -14,7 +14,7 @@ Then, write the following to `server.mjs`. (By using the .mjs extension, Node tr

```js
import { ApolloServer } from "@apollo/server";
import { startServerAndCreateLambdaHandler } from "@as-integrations/aws-lambda";
import { startServerAndCreateLambdaHandler, handlers } from "@as-integrations/aws-lambda";

// The GraphQL schema
const typeDefs = `#graphql
Expand All @@ -36,5 +36,264 @@ const server = new ApolloServer({
resolvers,
});

export default startServerAndCreateLambdaHandler(server);
```
export default startServerAndCreateLambdaHandler(
server,
handlers.createAPIGatewayProxyEventV2RequestHandler(),
);
```

## Middleware

For mutating the event before passing off to `@apollo/server` or mutating the result right before returning, middleware can be utilized.

> Note, this middleware is strictly for event and result mutations and should not be used for any GraphQL modification. For that, [plugins](https://www.apollographql.com/docs/apollo-server/builtin-plugins) from `@apollo/server` would be much better suited.

For example, if you need to set cookie headers with a V2 Proxy Result, see the following code example:

```typescript
import {
startServerAndCreateLambdaHandler,
handlers,
} from "@as-integrations/aws-lambda";
import type {
APIGatewayProxyEventV2,
} from 'aws-lambda';
import {server} from "./server";

async function regenerateCookie(event: APIGatewayProxyEventV2) {
// ...
return "NEW_COOKIE";
}

export default startServerAndCreateLambdaHandler(
server,
handlers.createAPIGatewayProxyEventV2RequestHandler(),
{
middleware: [
// Both event and result are intended to be mutable
async (event) => {
const cookie = await regenerateCookie(event);
return (result) => {
result.cookies.push(cookie);
}
}
]
}
);

```

If you want to define strictly typed middleware outside of the middleware array, the easiest way would be to extract your request handler into a variable and utilize the `typeof` keyword from Typescript. You could also manually use the `RequestHandler` type and fill in the event and result values yourself.

```typescript
import {
startServerAndCreateLambdaHandler,
middleware,
handlers,
} from "@as-integrations/aws-lambda";
import type {
APIGatewayProxyEventV2,
APIGatewayProxyStructuredResultV2,
} from 'aws-lambda';
import {server} from "./server";

const requestHandler = handlers.createAPIGatewayProxyEventV2RequestHandler();

// Utilizing typeof
const cookieMiddleware: middleware.MiddlewareFn<typeof requestHandler> = (event) => {
// ...
return (result) => {
// ...
}
}

// Manual event filling
const otherMiddleware: middleware.MiddlewareFn<
RequestHandler<APIGatewayProxyEventV2, APIGatewayProxyStructuredResultV2>
> = (event) => {
// ...
return (result) => {
// ...
}
}

export default startServerAndCreateLambdaHandler(
server,
requestHandler,
{
middleware: [
// cookieMiddleware will always work here as its signature is
// tied to the `requestHandler` above
cookieMiddleware,

// otherMiddleware will error if the event and result types do
// not sufficiently overlap, meaning it is your responsibility
// to keep the event types in sync, but the compiler may help
otherMiddleware,
],
}
);
```

## Event Extensions

Each of the provided request handler factories has a generic for you to pass a manually extended event type if you have custom authorizers, or if the event type you need has a generic you must pass yourself. For example, here is a request that allows access to the lambda authorizer:

```typescript
import {
startServerAndCreateLambdaHandler,
middleware,
handlers,
} from "@as-integrations/aws-lambda";
import type {
APIGatewayProxyEventV2WithLambdaAuthorizer,
} from 'aws-lambda';
import {server} from "./server";

export default startServerAndCreateLambdaHandler(
server,
handlers.createAPIGatewayProxyEventV2RequestHandler<
APIGatewayProxyEventV2WithLambdaAuthorizer<{
myAuthorizerContext: string
}>
>(), // This event will also be applied to the MiddlewareFn type
)

```

## Custom Request Handlers

When invoking a lambda manually, or when using an event source we don't currently support (feel free to create a PR), a custom request handler might be necessary. A request handler is created using the `handlers.createHandler` function which takes two function arguments `eventParser` and `resultGenerator`, and two type arguments `EventType` and `ResultType`.

### `eventParser` Argument

There are two type signatures available for parsing events:

#### Method A: Helper Object
This helper object has 4 properties that will complete a full parsing chain, and abstracts some of the work required to coerce the incoming event into a `HTTPGraphQLRequest`. This is the recommended way of parsing events.


##### `parseHttpMethod(event: EventType): string`

Returns the HTTP verb from the request.

Example return value: `GET`

##### `parseQueryParams(event: EventType): string`

Returns the raw query param string from the request. If the request comes in as a pre-mapped type, you may need to use `URLSearchParams` to re-stringify it.

Example return value: `foo=1&bar=2`

##### `parseHeaders(event: EventType): HeaderMap`

Import from here: `import {HeaderMap} from "@apollo/server"`;

Return an Apollo Server header map from the event. `HeaderMap` automatically normalizes casing for you.

##### `parseBody(event: EventType, headers: HeaderMap): string`

Return a plaintext body. Be sure to parse out any base64 or charset encoding. Headers are provided here for convenience as some body parsing might be dependent on `content-type`

#### Method B: Parser Function

If the helper object is too restrictive for your use-case, the other option is to create a function with `(event: EventType): HTTPGraphQLRequest` as the signature. Here you can do any parsing and it is your responsibility to create a valid `HTTPGraphQLRequest`.

### `resultGenerator` Argument

There are two possible result types, `success` and `error`, and they are to be defined as function properties on an object. Middleware will _always_ run, regardless if the generated result was from a success of error. The properties have the following signatures:

##### `success(response: HTTPGraphQLResponse): ResultType`

Given a complete response, generate the desired result type.

##### `error(e: unknown): ResultType`

Given an unknown type error, generate a result. If you want to create a basic parser that captures everything, utilize the instanceof type guard from Typescript.

```typescript
error(e) {
if(e instanceof Error) {
return {
...
}
}
// If error cannot be determined, panic and use lambda's default error handler
// Might be advantageous to add extra logging here so unexpected errors can be properly handled later
throw e;
}
```

### Custom Handler Example

```typescript

import {
startServerAndCreateLambdaHandler,
handlers,
} from "@as-integrations/aws-lambda";
import type {
APIGatewayProxyEventV2,
} from 'aws-lambda';
import {HeaderMap} from "@apollo/server";
import {server} from "./server";

type CustomInvokeEvent = {
httpMethod: string;
queryParams: string;
headers: Record<string, string>,
body: string,
}

type CustomInvokeResult = {
success: true;
body: string;
} | {
success: false;
error: string;
}


const requestHandler = handlers.createRequestHandler<CustomInvokeEvent, CustomInvokeResult>({
parseHttpMethod(event) {
return event.httpMethod;
},
parseHeaders(event) {
const headerMap = new HeaderMap();
for(const [key, value] of Object.entries(event.headers)) {
headerMap.set(key, value);
}
return headerMap;
},
parseQueryParams(event) {
return event.queryParams;
},
parseBody(event) {
return event.body;
},
}, {
success({body}) {
return {
success: true,
body: body.string,
}
},
error(e) {
if(e instanceof Error) {
return {
success: false,
error: e.toString(),
}
}
console.error('Unknown error type encountered!', e);
throw e;
}
})

export default startServerAndCreateLambdaHandler(
server,
requestHandler,
);

```

2 changes: 2 additions & 0 deletions cspell-dict.txt
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,5 @@ testsuite
unawaited
vendia
withrequired
typeof
instanceof
29 changes: 20 additions & 9 deletions src/__tests__/defineLambdaTestSuite.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,24 @@ import {
CreateServerForIntegrationTestsOptions,
defineIntegrationTestSuite,
} from '@apollo/server-integration-testsuite';
import type { Handler } from 'aws-lambda';
import { createServer, IncomingMessage, ServerResponse } from 'http';
import { startServerAndCreateLambdaHandler } from '..';
import {
LambdaHandler,
startServerAndCreateLambdaHandler,
middleware,
handlers,
} from '..';
import { urlForHttpServer } from './mockServer';

export function defineLambdaTestSuite<Event, Response>(
export function defineLambdaTestSuite<
RH extends handlers.RequestHandler<any, any>,
>(
options: {
requestHandler: RH;
middleware?: Array<middleware.MiddlewareFn<RH>>;
},
mockServerFactory: (
handler: Handler<Event, Response>,
handler: LambdaHandler<RH>,
shouldBase64Encode: boolean,
) => (req: IncomingMessage, res: ServerResponse) => void,
) {
Expand All @@ -29,15 +39,16 @@ export function defineLambdaTestSuite<Event, Response>(

const handler = startServerAndCreateLambdaHandler(
server,
testOptions,
options.requestHandler,
{
...testOptions,
middleware: options.middleware,
},
);

httpServer.addListener(
'request',
mockServerFactory(
handler as Handler<Event, Response>,
shouldBase64Encode,
),
mockServerFactory(handler, shouldBase64Encode),
);

await new Promise<void>((resolve) => {
Expand Down
6 changes: 5 additions & 1 deletion src/__tests__/integrationALB.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import { createMockALBServer } from './mockALBServer';
import { defineLambdaTestSuite } from './defineLambdaTestSuite';
import { handlers } from '..';

describe('lambdaHandlerALB', () => {
defineLambdaTestSuite(createMockALBServer);
defineLambdaTestSuite(
{ requestHandler: handlers.createALBEventRequestHandler() },
createMockALBServer,
);
});
6 changes: 5 additions & 1 deletion src/__tests__/integrationV1.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import { handlers } from '..';
import { defineLambdaTestSuite } from './defineLambdaTestSuite';
import { createMockV1Server } from './mockAPIGatewayV1Server';

describe('lambdaHandlerV1', () => {
defineLambdaTestSuite(createMockV1Server);
defineLambdaTestSuite(
{ requestHandler: handlers.createAPIGatewayProxyEventRequestHandler() },
createMockV1Server,
);
});
6 changes: 5 additions & 1 deletion src/__tests__/integrationV2.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import { handlers } from '..';
import { defineLambdaTestSuite } from './defineLambdaTestSuite';
import { createMockV2Server } from './mockAPIGatewayV2Server';

describe('lambdaHandlerV2', () => {
defineLambdaTestSuite(createMockV2Server);
defineLambdaTestSuite(
{ requestHandler: handlers.createAPIGatewayProxyEventV2RequestHandler() },
createMockV2Server,
);
});
Loading