Skip to content

Commit 803f8d9

Browse files
authored
feat(parser): implement middy parser middleware (#1823)
* add middy middleware * add type to imports * remove schema type, stick with unkown
1 parent ce9f8a1 commit 803f8d9

File tree

5 files changed

+209
-1
lines changed

5 files changed

+209
-1
lines changed

Diff for: packages/parser/src/middleware/parser.ts

+56
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { type MiddyLikeRequest } from '@aws-lambda-powertools/commons/types';
2+
import { type MiddlewareObj } from '@middy/core';
3+
import { type ZodSchema } from 'zod';
4+
import { type 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+
* ```typescript
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 };

Diff for: packages/parser/src/types/envelope.ts

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

Diff for: packages/parser/tests/unit/envelopes/eventbridge.test.ts

+1-1
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/eventbridge.js';
10+
import { eventBridgeEnvelope } from '../../../src/envelopes/event-bridge.js';
1111

1212
describe('EventBridgeEnvelope ', () => {
1313
it('should parse eventbridge event', () => {

Diff for: packages/parser/tests/unit/parser.test.ts

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

0 commit comments

Comments
 (0)