Skip to content

Commit 5669d23

Browse files
authored
Request Handlers, Custom Extensions, and Middleware, Oh My! (#67)
Created custom handler structure and middleware implementation
1 parent dff70b6 commit 5669d23

20 files changed

+844
-225
lines changed

.changeset/chilled-teachers-bathe.md

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
---
2+
'@as-integrations/aws-lambda': major
3+
---
4+
5+
## Why Change?
6+
7+
In the interest of supporting more event types and allowing user-extensibility, the event parsing has been rearchitected. The goal with v2.0 is to allow customizability at each step in the event pipeline, leading to a higher level of Lambda event coverage (including 100% custom event requests).
8+
9+
## What changed?
10+
11+
The second parameter introduces a handler that controls parsing and output generation based on the event type you are consuming. We support 3 event types out-of-the-box: APIGatewayProxyV1/V2 and ALB. Additionally, there is a function for creating your own event parsers in case the pre-defined ones are not sufficient.
12+
13+
This update also introduces middleware, a great way to modify the request on the way in or update the result on the way out.
14+
15+
```typescript
16+
startServerAndCreateLambdaHandler(
17+
server,
18+
handlers.createAPIGatewayProxyEventV2RequestHandler(),
19+
{
20+
middleware: [
21+
async (event) => {
22+
// event updates here
23+
return async (result) => {
24+
// result updates here
25+
};
26+
},
27+
],
28+
},
29+
);
30+
```
31+
32+
## Upgrade Path
33+
34+
The upgrade from v1.x to v2.0.0 is quite simple, just update your `startServerAndCreateLambdaHandler` with the new request handler parameter. Example:
35+
36+
```typescript
37+
import {
38+
startServerAndCreateLambdaHandler,
39+
handlers,
40+
} from '@as-integrations/aws-lambda';
41+
42+
export default startServerAndCreateLambdaHandler(
43+
server,
44+
handlers.createAPIGatewayProxyEventV2RequestHandler(),
45+
);
46+
```
47+
48+
The 3 event handlers provided by the package are:
49+
50+
- `createAPIGatewayProxyEventV2RequestHandler()`
51+
- `createALBEventRequestHandler()`
52+
- `createAPIGatewayProxyEventRequestHandler()`
53+
54+
Each of these have an optional type parameter which you can use to extend the base event. This is useful if you are using Lambda functions with custom authorizers and need additional context in your events.
55+
56+
Creating your own event parsers is now possible with `handlers.createRequestHandler()`. Creation of custom handlers is documented in the README.

.prettierignore

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
*.json
22
*.json5
33
*.yml
4-
*.md
54

65
.volta
76

.prettierrc

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
{
22
"trailingComma": "all",
3-
"singleQuote": true
3+
"singleQuote": true,
4+
"proseWrap": "preserve"
45
}

README.md

Lines changed: 260 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
# `@as-integrations/aws-lambda`
22

3-
## Getting started: Lambda middleware
3+
## Getting started
44

5-
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!
5+
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 comes with built-in request handling functionality for ProxyV1, ProxyV2, and ALB events [with extensible typing](#event-extensions). You can also create your own integrations via a [Custom Handler](#custom-request-handlers) and submitted as a PR if others might find them valuable.
66

77
First, install Apollo Server, graphql-js, and the Lambda handler package:
88

@@ -13,8 +13,11 @@ npm install @apollo/server graphql @as-integrations/aws-lambda
1313
Then, write the following to `server.mjs`. (By using the .mjs extension, Node treats the file as a module, allowing us to use ESM `import` syntax.)
1414

1515
```js
16-
import { ApolloServer } from "@apollo/server";
17-
import { startServerAndCreateLambdaHandler } from "@as-integrations/aws-lambda";
16+
import { ApolloServer } from '@apollo/server';
17+
import {
18+
startServerAndCreateLambdaHandler,
19+
handlers,
20+
} from '@as-integrations/aws-lambda';
1821

1922
// The GraphQL schema
2023
const typeDefs = `#graphql
@@ -26,7 +29,7 @@ const typeDefs = `#graphql
2629
// A map of functions which return data for the schema.
2730
const resolvers = {
2831
Query: {
29-
hello: () => "world",
32+
hello: () => 'world',
3033
},
3134
};
3235

@@ -36,5 +39,255 @@ const server = new ApolloServer({
3639
resolvers,
3740
});
3841

39-
export default startServerAndCreateLambdaHandler(server);
40-
```
42+
export default startServerAndCreateLambdaHandler(
43+
server,
44+
handlers.createAPIGatewayProxyEventV2RequestHandler(),
45+
);
46+
```
47+
48+
## Middleware
49+
50+
For mutating the event before passing off to `@apollo/server` or mutating the result right before returning, middleware can be utilized.
51+
52+
> 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.
53+
54+
For example, if you need to set cookie headers with a V2 Proxy Result, see the following code example:
55+
56+
```typescript
57+
import {
58+
startServerAndCreateLambdaHandler,
59+
handlers,
60+
} from '@as-integrations/aws-lambda';
61+
import type { APIGatewayProxyEventV2 } from 'aws-lambda';
62+
import { server } from './server';
63+
64+
async function regenerateCookie(event: APIGatewayProxyEventV2) {
65+
// ...
66+
return 'NEW_COOKIE';
67+
}
68+
69+
export default startServerAndCreateLambdaHandler(
70+
server,
71+
handlers.createAPIGatewayProxyEventV2RequestHandler(),
72+
{
73+
middleware: [
74+
// Both event and result are intended to be mutable
75+
async (event) => {
76+
const cookie = await regenerateCookie(event);
77+
return (result) => {
78+
result.cookies.push(cookie);
79+
};
80+
},
81+
],
82+
},
83+
);
84+
```
85+
86+
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.
87+
88+
```typescript
89+
import {
90+
startServerAndCreateLambdaHandler,
91+
middleware,
92+
handlers,
93+
} from '@as-integrations/aws-lambda';
94+
import type {
95+
APIGatewayProxyEventV2,
96+
APIGatewayProxyStructuredResultV2,
97+
} from 'aws-lambda';
98+
import { server } from './server';
99+
100+
const requestHandler = handlers.createAPIGatewayProxyEventV2RequestHandler();
101+
102+
// Utilizing typeof
103+
const cookieMiddleware: middleware.MiddlewareFn<typeof requestHandler> = (
104+
event,
105+
) => {
106+
// ...
107+
return (result) => {
108+
// ...
109+
};
110+
};
111+
112+
// Manual event filling
113+
const otherMiddleware: middleware.MiddlewareFn<
114+
RequestHandler<APIGatewayProxyEventV2, APIGatewayProxyStructuredResultV2>
115+
> = (event) => {
116+
// ...
117+
return (result) => {
118+
// ...
119+
};
120+
};
121+
122+
export default startServerAndCreateLambdaHandler(server, requestHandler, {
123+
middleware: [
124+
// cookieMiddleware will always work here as its signature is
125+
// tied to the `requestHandler` above
126+
cookieMiddleware,
127+
128+
// otherMiddleware will error if the event and result types do
129+
// not sufficiently overlap, meaning it is your responsibility
130+
// to keep the event types in sync, but the compiler may help
131+
otherMiddleware,
132+
],
133+
});
134+
```
135+
136+
## Event Extensions
137+
138+
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:
139+
140+
```typescript
141+
import {
142+
startServerAndCreateLambdaHandler,
143+
middleware,
144+
handlers,
145+
} from '@as-integrations/aws-lambda';
146+
import type { APIGatewayProxyEventV2WithLambdaAuthorizer } from 'aws-lambda';
147+
import { server } from './server';
148+
149+
export default startServerAndCreateLambdaHandler(
150+
server,
151+
handlers.createAPIGatewayProxyEventV2RequestHandler<
152+
APIGatewayProxyEventV2WithLambdaAuthorizer<{
153+
myAuthorizerContext: string;
154+
}>
155+
>(), // This event will also be applied to the MiddlewareFn type
156+
);
157+
```
158+
159+
## Custom Request Handlers
160+
161+
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`.
162+
163+
### `eventParser` Argument
164+
165+
There are two type signatures available for parsing events:
166+
167+
#### Method A: Helper Object
168+
169+
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.
170+
171+
##### `parseHttpMethod(event: EventType): string`
172+
173+
Returns the HTTP verb from the request.
174+
175+
Example return value: `GET`
176+
177+
##### `parseQueryParams(event: EventType): string`
178+
179+
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.
180+
181+
Example return value: `foo=1&bar=2`
182+
183+
##### `parseHeaders(event: EventType): HeaderMap`
184+
185+
Import from here: `import {HeaderMap} from "@apollo/server"`;
186+
187+
Return an Apollo Server header map from the event. `HeaderMap` automatically normalizes casing for you.
188+
189+
##### `parseBody(event: EventType, headers: HeaderMap): string`
190+
191+
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`
192+
193+
#### Method B: Parser Function
194+
195+
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`.
196+
197+
### `resultGenerator` Argument
198+
199+
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 or error. The properties have the following signatures:
200+
201+
##### `success(response: HTTPGraphQLResponse): ResultType`
202+
203+
Given a complete response, generate the desired result type.
204+
205+
##### `error(e: unknown): ResultType`
206+
207+
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.
208+
209+
```typescript
210+
error(e) {
211+
if(e instanceof Error) {
212+
return {
213+
...
214+
}
215+
}
216+
// If error cannot be determined, panic and use lambda's default error handler
217+
// Might be advantageous to add extra logging here so unexpected errors can be properly handled later
218+
throw e;
219+
}
220+
```
221+
222+
### Custom Handler Example
223+
224+
```typescript
225+
import {
226+
startServerAndCreateLambdaHandler,
227+
handlers,
228+
} from '@as-integrations/aws-lambda';
229+
import type { APIGatewayProxyEventV2 } from 'aws-lambda';
230+
import { HeaderMap } from '@apollo/server';
231+
import { server } from './server';
232+
233+
type CustomInvokeEvent = {
234+
httpMethod: string;
235+
queryParams: string;
236+
headers: Record<string, string>;
237+
body: string;
238+
};
239+
240+
type CustomInvokeResult =
241+
| {
242+
success: true;
243+
body: string;
244+
}
245+
| {
246+
success: false;
247+
error: string;
248+
};
249+
250+
const requestHandler = handlers.createRequestHandler<
251+
CustomInvokeEvent,
252+
CustomInvokeResult
253+
>(
254+
{
255+
parseHttpMethod(event) {
256+
return event.httpMethod;
257+
},
258+
parseHeaders(event) {
259+
const headerMap = new HeaderMap();
260+
for (const [key, value] of Object.entries(event.headers)) {
261+
headerMap.set(key, value);
262+
}
263+
return headerMap;
264+
},
265+
parseQueryParams(event) {
266+
return event.queryParams;
267+
},
268+
parseBody(event) {
269+
return event.body;
270+
},
271+
},
272+
{
273+
success({ body }) {
274+
return {
275+
success: true,
276+
body: body.string,
277+
};
278+
},
279+
error(e) {
280+
if (e instanceof Error) {
281+
return {
282+
success: false,
283+
error: e.toString(),
284+
};
285+
}
286+
console.error('Unknown error type encountered!', e);
287+
throw e;
288+
},
289+
},
290+
);
291+
292+
export default startServerAndCreateLambdaHandler(server, requestHandler);
293+
```

cspell-dict.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,5 @@ testsuite
1010
unawaited
1111
vendia
1212
withrequired
13+
typeof
14+
instanceof

0 commit comments

Comments
 (0)