Skip to content

Commit 71c94df

Browse files
authored
Updated middleware execution to provide new functionality (#91)
* Updated middleware execution to provide new functionality * Add missing headers to the changeset * Remove context modification from result middleware * Update from review * Added new README sections and changelog message * Spelling * Prettier
1 parent e76fc08 commit 71c94df

File tree

6 files changed

+235
-23
lines changed

6 files changed

+235
-23
lines changed

.changeset/giant-rats-rhyme.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
---
2+
'@as-integrations/aws-lambda': minor
3+
---
4+
5+
## Short circuit middleware execution
6+
7+
You can now opt to return a Lambda result object directly from the middleware. This will cancel the middleware chain, bypass GraphQL request processing, and immediately return the Lambda result.
8+
9+
Example
10+
11+
```ts
12+
export const handler = startServerAndCreateLambdaHandler(
13+
server,
14+
handlers.createAPIGatewayProxyEventV2RequestHandler(),
15+
{
16+
context: async () => {
17+
return {};
18+
},
19+
middleware: [
20+
async (event) => {
21+
const psk = Buffer.from('SuperSecretPSK');
22+
const token = Buffer.from(event.headers['X-Auth-Token']);
23+
if (
24+
psk.byteLength !== token.byteLength ||
25+
crypto.timingSafeEqual(psk, token)
26+
) {
27+
return {
28+
statusCode: '403',
29+
body: 'Forbidden',
30+
};
31+
}
32+
},
33+
],
34+
},
35+
);
36+
```

README.md

Lines changed: 97 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import {
2222
// The GraphQL schema
2323
const typeDefs = `#graphql
2424
type Query {
25-
hello: String
25+
hello: String!
2626
}
2727
`;
2828

@@ -45,6 +45,59 @@ export default startServerAndCreateLambdaHandler(
4545
);
4646
```
4747

48+
## Context
49+
50+
As with all Apollo Server 4 integrations, the context resolution is done in the integration. For the Lambda integration, it will look like the following:
51+
52+
```ts
53+
import { ApolloServer } from '@apollo/server';
54+
import {
55+
startServerAndCreateLambdaHandler,
56+
handlers,
57+
} from '@as-integrations/aws-lambda';
58+
59+
type ContextValue = {
60+
isAuthenticated: boolean;
61+
};
62+
63+
// The GraphQL schema
64+
const typeDefs = `#graphql
65+
type Query {
66+
hello: String!
67+
isAuthenticated: Boolean!
68+
}
69+
`;
70+
71+
// Set up Apollo Server
72+
const server = new ApolloServer<ContextValue>({
73+
typeDefs,
74+
resolvers: {
75+
Query: {
76+
hello: () => 'world',
77+
isAuthenticated: (root, args, context) => {
78+
// For context typing to be valid one of the following must be implemented
79+
// 1. `resolvers` defined inline in the server config (not particularly scalable, but works)
80+
// 2. Add the type in the resolver function. ex. `(root, args, context: ContextValue)`
81+
// 3. Propagate the type from an outside definition like GraphQL Codegen
82+
return context.isAuthenticated;
83+
},
84+
},
85+
},
86+
});
87+
88+
export default startServerAndCreateLambdaHandler(
89+
server,
90+
handlers.createAPIGatewayProxyEventV2RequestHandler({
91+
context: async ({ event }) => {
92+
// Do some parsing on the event (parse JWT, cookie, auth header, etc.)
93+
return {
94+
isAuthenticated: true,
95+
};
96+
},
97+
}),
98+
);
99+
```
100+
48101
## Middleware
49102

50103
For mutating the event before passing off to `@apollo/server` or mutating the result right before returning, middleware can be utilized.
@@ -83,6 +136,8 @@ export default startServerAndCreateLambdaHandler(
83136
);
84137
```
85138

139+
### Middleware Typing
140+
86141
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.
87142

88143
```typescript
@@ -133,6 +188,47 @@ export default startServerAndCreateLambdaHandler(server, requestHandler, {
133188
});
134189
```
135190

191+
### Middleware Short Circuit
192+
193+
In some situations, a middleware function might require the execution end before reaching Apollo Server. This might be a global auth guard or session token lookup.
194+
195+
To achieve this, the request middleware function accepts `ResultType` or `Promise<ResultType>` as a return type. Should middleware resolve to such a value, that result is returned and no further execution occurs.
196+
197+
```typescript
198+
import {
199+
startServerAndCreateLambdaHandler,
200+
middleware,
201+
handlers,
202+
} from '@as-integrations/aws-lambda';
203+
import type {
204+
APIGatewayProxyEventV2,
205+
APIGatewayProxyStructuredResultV2,
206+
} from 'aws-lambda';
207+
import { server } from './server';
208+
209+
const requestHandler = handlers.createAPIGatewayProxyEventV2RequestHandler();
210+
211+
// Utilizing typeof
212+
const sessionMiddleware: middleware.MiddlewareFn<typeof requestHandler> = (
213+
event,
214+
) => {
215+
// ... check session
216+
if (!event.headers['X-Session-Key']) {
217+
// If header doesn't exist, return early
218+
return {
219+
statusCode: 401
220+
body: 'Unauthorized'
221+
}
222+
}
223+
};
224+
225+
export default startServerAndCreateLambdaHandler(server, requestHandler, {
226+
middleware: [
227+
sessionMiddleware,
228+
],
229+
});
230+
```
231+
136232
## Event Extensions
137233

138234
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:

cspell-dict.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,5 @@ unawaited
1212
vendia
1313
withrequired
1414
typeof
15-
instanceof
15+
instanceof
16+
codegen

src/__tests__/middleware.test.ts

Lines changed: 82 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,54 @@
11
import { ApolloServer } from '@apollo/server';
22
import type { APIGatewayProxyEventV2 } from 'aws-lambda';
33
import { handlers, startServerAndCreateLambdaHandler } from '..';
4+
import gql from 'graphql-tag';
5+
import { type DocumentNode, print } from 'graphql';
46

5-
const event: APIGatewayProxyEventV2 = {
6-
version: '2',
7-
headers: {
8-
'content-type': 'application/json',
9-
},
10-
isBase64Encoded: false,
11-
rawQueryString: '',
12-
requestContext: {
13-
http: {
14-
method: 'POST',
7+
function createEvent(doc: DocumentNode): APIGatewayProxyEventV2 {
8+
return {
9+
version: '2',
10+
headers: {
11+
'content-type': 'application/json',
1512
},
16-
// Other requestContext properties omitted for brevity
17-
} as any,
18-
rawPath: '/',
19-
routeKey: '/',
20-
body: '{"operationName": null, "variables": null, "query": "{ hello }"}',
21-
};
13+
isBase64Encoded: false,
14+
rawQueryString: '',
15+
requestContext: {
16+
http: {
17+
method: 'POST',
18+
},
19+
// Other requestContext properties omitted for brevity
20+
} as any,
21+
rawPath: '/',
22+
routeKey: '/',
23+
body: JSON.stringify({
24+
query: print(doc),
25+
}),
26+
};
27+
}
2228

2329
const typeDefs = `#graphql
2430
type Query {
2531
hello: String
2632
}
33+
type Mutation {
34+
mutateContext: String
35+
}
2736
`;
2837

2938
const resolvers = {
3039
Query: {
3140
hello: () => 'world',
3241
},
42+
Mutation: {
43+
mutateContext: async (
44+
_root: any,
45+
_args: any,
46+
context: { foo: string | null },
47+
) => {
48+
context.foo = 'bar';
49+
return 'ok';
50+
},
51+
},
3352
};
3453

3554
const server = new ApolloServer({
@@ -39,13 +58,23 @@ const server = new ApolloServer({
3958

4059
describe('Request mutation', () => {
4160
it('updates incoming event headers', async () => {
61+
const event = createEvent(gql`
62+
query {
63+
hello
64+
}
65+
`);
4266
const headerAdditions = {
4367
'x-injected-header': 'foo',
4468
};
4569
const lambdaHandler = startServerAndCreateLambdaHandler(
4670
server,
4771
handlers.createAPIGatewayProxyEventV2RequestHandler(),
4872
{
73+
context: async () => {
74+
return {
75+
foo: null,
76+
};
77+
},
4978
middleware: [
5079
async (event) => {
5180
Object.assign(event.headers, headerAdditions);
@@ -58,15 +87,52 @@ describe('Request mutation', () => {
5887
expect(event.headers[key]).toBe(value);
5988
}
6089
});
90+
it('returns early if middleware returns a result', async () => {
91+
const event = createEvent(gql`
92+
query {
93+
hello
94+
}
95+
`);
96+
const lambdaHandler = startServerAndCreateLambdaHandler(
97+
server,
98+
handlers.createAPIGatewayProxyEventV2RequestHandler(),
99+
{
100+
context: async () => {
101+
return {
102+
foo: null,
103+
};
104+
},
105+
middleware: [
106+
async () => {
107+
return {
108+
statusCode: 418,
109+
};
110+
},
111+
],
112+
},
113+
);
114+
const result = await lambdaHandler(event, {} as any, () => {})!;
115+
expect(result.statusCode).toBe(418);
116+
});
61117
});
62118

63119
describe('Response mutation', () => {
64120
it('adds cookie values to emitted result', async () => {
121+
const event = createEvent(gql`
122+
query {
123+
hello
124+
}
125+
`);
65126
const cookieValue = 'foo=bar';
66127
const lambdaHandler = startServerAndCreateLambdaHandler(
67128
server,
68129
handlers.createAPIGatewayProxyEventV2RequestHandler(),
69130
{
131+
context: async () => {
132+
return {
133+
foo: null,
134+
};
135+
},
70136
middleware: [
71137
async () => {
72138
return async (result) => {

src/lambdaHandler.ts

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -75,17 +75,30 @@ export function startServerAndCreateLambdaHandler<
7575
[];
7676
try {
7777
for (const middlewareFn of options?.middleware ?? []) {
78-
const resultCallback = await middlewareFn(event);
79-
if (resultCallback) {
80-
resultMiddlewareFns.push(resultCallback);
78+
const middlewareReturnValue = await middlewareFn(event);
79+
// If the middleware returns an object, we assume it's a LambdaResponse
80+
if (
81+
typeof middlewareReturnValue === 'object' &&
82+
middlewareReturnValue !== null
83+
) {
84+
return middlewareReturnValue;
85+
}
86+
// If the middleware returns a function, we assume it's a result callback
87+
if (middlewareReturnValue) {
88+
resultMiddlewareFns.push(middlewareReturnValue);
8189
}
8290
}
8391

8492
const httpGraphQLRequest = handler.fromEvent(event);
8593

8694
const response = await server.executeHTTPGraphQLRequest({
8795
httpGraphQLRequest,
88-
context: () => contextFunction({ event, context }),
96+
context: () => {
97+
return contextFunction({
98+
event,
99+
context,
100+
});
101+
},
89102
});
90103

91104
const result = handler.toSuccessResult(response);

src/middleware.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ export type LambdaResponse<ResultType> = (result: ResultType) => Promise<void>;
44

55
export type LambdaRequest<EventType, ResultType> = (
66
event: EventType,
7-
) => Promise<LambdaResponse<ResultType> | void>;
7+
) => Promise<LambdaResponse<ResultType> | ResultType | void>;
88

99
export type MiddlewareFn<RH extends RequestHandler<any, any>> =
1010
RH extends RequestHandler<infer EventType, infer ResultType>

0 commit comments

Comments
 (0)