Skip to content

Commit 58ee5fb

Browse files
committed
add middy middleware
1 parent ba14813 commit 58ee5fb

File tree

5 files changed

+208
-1
lines changed

5 files changed

+208
-1
lines changed
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { MiddyLikeRequest } from '@aws-lambda-powertools/commons/types';
2+
import { MiddlewareObj } from '@middy/core';
3+
import { ZodSchema } from 'zod';
4+
import { Envelope } from '../types/envelope.js';
5+
6+
interface ParserOptions<S extends ZodSchema> {
7+
schema: S;
8+
envelope?: Envelope;
9+
}
10+
11+
/**
12+
* A middiy middleware to parse your event.
13+
*
14+
* @exmaple
15+
* ```typescirpt
16+
* import { parser } from '@aws-lambda-powertools/parser/middleware';
17+
* import middy from '@middy/core';
18+
* import { sqsEnvelope } from '@aws-lambda-powertools/parser/envelopes/sqs;'
19+
*
20+
* const oderSchema = z.object({
21+
* id: z.number(),
22+
* description: z.string(),
23+
* quantity: z.number(),
24+
* });
25+
*
26+
* type Order = z.infer<typeof oderSchema>;
27+
*
28+
* export const handler = middy(
29+
* async (event: Order, _context: unknown): Promise<void> => {
30+
* // event is validated as sqs message envelope
31+
* // the body is unwrapped and parsed into object ready to use
32+
* // you can now use event as Order in your code
33+
* }
34+
* ).use(parser({ schema: oderSchema, envelope: sqsEnvelope }));
35+
* ```
36+
*
37+
* @param options
38+
*/
39+
const parser = <S extends ZodSchema>(
40+
options: ParserOptions<S>
41+
): MiddlewareObj => {
42+
const before = (request: MiddyLikeRequest): void => {
43+
const { schema, envelope } = options;
44+
if (envelope) {
45+
request.event = envelope(request.event, schema);
46+
} else {
47+
request.event = schema.parse(request.event);
48+
}
49+
};
50+
51+
return {
52+
before,
53+
};
54+
};
55+
56+
export { parser };

packages/parser/src/types/envelope.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { apiGatewayEnvelope } from '../envelopes/apigw.js';
2+
import { apiGatewayV2Envelope } from '../envelopes/apigwv2.js';
3+
import { cloudWatchEnvelope } from '../envelopes/cloudwatch.js';
4+
import { dynamoDDStreamEnvelope } from '../envelopes/dynamodb.js';
5+
import { kafkaEnvelope } from '../envelopes/kafka.js';
6+
import { kinesisEnvelope } from '../envelopes/kinesis.js';
7+
import { kinesisFirehoseEnvelope } from '../envelopes/kinesis-firehose.js';
8+
import { lambdaFunctionUrlEnvelope } from '../envelopes/lambda.js';
9+
import { snsEnvelope } from '../envelopes/sns.js';
10+
import { snsSqsEnvelope } from '../envelopes/sns.js';
11+
import { sqsEnvelope } from '../envelopes/sqs.js';
12+
import { vpcLatticeEnvelope } from '../envelopes/vpc-lattice.js';
13+
import { vpcLatticeV2Envelope } from '../envelopes/vpc-latticev2.js';
14+
import { eventBridgeEnvelope } from '../envelopes/event-bridge.js';
15+
16+
export type Envelope =
17+
| typeof apiGatewayEnvelope
18+
| typeof apiGatewayV2Envelope
19+
| typeof cloudWatchEnvelope
20+
| typeof dynamoDDStreamEnvelope
21+
| typeof eventBridgeEnvelope
22+
| typeof kafkaEnvelope
23+
| typeof kinesisEnvelope
24+
| typeof kinesisFirehoseEnvelope
25+
| typeof lambdaFunctionUrlEnvelope
26+
| typeof snsEnvelope
27+
| typeof snsSqsEnvelope
28+
| typeof sqsEnvelope
29+
| typeof vpcLatticeEnvelope
30+
| typeof vpcLatticeV2Envelope;

packages/parser/tests/unit/envelopes/eventbridge.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
import { TestEvents, TestSchema } from '../schema/utils.js';
88
import { generateMock } from '@anatine/zod-mock';
99
import { EventBridgeEvent } from 'aws-lambda';
10-
import { eventBridgeEnvelope } from '../../../src/envelopes/eventBridgeEnvelope';
10+
import { eventBridgeEnvelope } from '../../../src/envelopes/event-bridge.js';
1111

1212
describe('EventBridgeEnvelope ', () => {
1313
it('should parse eventbridge event', () => {
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
/**
2+
* Test middelware parser
3+
*
4+
* @group unit/parser
5+
*/
6+
7+
import middy from '@middy/core';
8+
import { Context } from 'aws-lambda';
9+
import { parser } from '../../src/middleware/parser.js';
10+
import { generateMock } from '@anatine/zod-mock';
11+
import { SqsSchema } from '../../src/schemas/sqs.js';
12+
import { z, ZodSchema } from 'zod';
13+
import { sqsEnvelope } from '../../src/envelopes/sqs';
14+
15+
describe('Middleware: parser', () => {
16+
const schema = z.object({
17+
name: z.string(),
18+
age: z.number().min(18).max(99),
19+
});
20+
type schema = z.infer<typeof schema>;
21+
const handler = async (
22+
event: schema | unknown,
23+
_context: Context
24+
): Promise<schema | unknown> => {
25+
return event;
26+
};
27+
28+
describe(' when envelope is provided ', () => {
29+
const middyfiedHandler = middy(handler).use(
30+
parser({ schema: schema, envelope: sqsEnvelope })
31+
);
32+
33+
it('should parse request body with schema and envelope', async () => {
34+
const bodyMock = generateMock(schema);
35+
36+
const event = generateMock(SqsSchema, {
37+
stringMap: {
38+
body: () => JSON.stringify(bodyMock),
39+
},
40+
});
41+
42+
const result = (await middyfiedHandler(event, {} as Context)) as schema[];
43+
result.forEach((item) => {
44+
expect(item).toEqual(bodyMock);
45+
});
46+
});
47+
48+
it('should throw when envelope does not match', async () => {
49+
await expect(async () => {
50+
await middyfiedHandler({ name: 'John', age: 18 }, {} as Context);
51+
}).rejects.toThrowError();
52+
});
53+
54+
it('should throw when schema does not match', async () => {
55+
const event = generateMock(SqsSchema, {
56+
stringMap: {
57+
body: () => '42',
58+
},
59+
});
60+
61+
await expect(middyfiedHandler(event, {} as Context)).rejects.toThrow();
62+
});
63+
it('should throw when provided schema is invalid', async () => {
64+
const middyfiedHandler = middy(handler).use(
65+
parser({ schema: {} as ZodSchema, envelope: sqsEnvelope })
66+
);
67+
68+
await expect(middyfiedHandler(42, {} as Context)).rejects.toThrowError();
69+
});
70+
it('should throw when envelope is correct but schema is invalid', async () => {
71+
const event = generateMock(SqsSchema, {
72+
stringMap: {
73+
body: () => JSON.stringify({ name: 'John', foo: 'bar' }),
74+
},
75+
});
76+
77+
const middyfiedHandler = middy(handler).use(
78+
parser({ schema: {} as ZodSchema, envelope: sqsEnvelope })
79+
);
80+
81+
await expect(
82+
middyfiedHandler(event, {} as Context)
83+
).rejects.toThrowError();
84+
});
85+
});
86+
87+
describe(' when envelope is not provided', () => {
88+
it('should parse the event with built-in schema', async () => {
89+
const event = generateMock(SqsSchema);
90+
91+
const middyfiedHandler = middy(handler).use(
92+
parser({ schema: SqsSchema })
93+
);
94+
95+
expect(await middyfiedHandler(event, {} as Context)).toEqual(event);
96+
});
97+
98+
it('should parse custom event', async () => {
99+
const event = { name: 'John', age: 18 };
100+
const middyfiedHandler = middy(handler).use(parser({ schema }));
101+
102+
expect(await middyfiedHandler(event, {} as Context)).toEqual(event);
103+
});
104+
105+
it('should throw when the schema does not match', async () => {
106+
const middyfiedHandler = middy(handler).use(parser({ schema }));
107+
108+
await expect(middyfiedHandler(42, {} as Context)).rejects.toThrow();
109+
});
110+
111+
it('should throw when provided schema is invalid', async () => {
112+
const middyfiedHandler = middy(handler).use(
113+
parser({ schema: {} as ZodSchema })
114+
);
115+
116+
await expect(
117+
middyfiedHandler({ foo: 'bar' }, {} as Context)
118+
).rejects.toThrowError();
119+
});
120+
});
121+
});

0 commit comments

Comments
 (0)