Skip to content

Commit

Permalink
feat(parser): implement middy parser middleware (#1823)
Browse files Browse the repository at this point in the history
* add middy middleware

* add type to imports

* remove schema type, stick with unkown
  • Loading branch information
am29d authored Dec 20, 2023
1 parent ce9f8a1 commit 803f8d9
Show file tree
Hide file tree
Showing 5 changed files with 209 additions and 1 deletion.
56 changes: 56 additions & 0 deletions packages/parser/src/middleware/parser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { type MiddyLikeRequest } from '@aws-lambda-powertools/commons/types';
import { type MiddlewareObj } from '@middy/core';
import { type ZodSchema } from 'zod';
import { type Envelope } from '../types/envelope.js';

interface ParserOptions<S extends ZodSchema> {
schema: S;
envelope?: Envelope;
}

/**
* A middiy middleware to parse your event.
*
* @exmaple
* ```typescript
* import { parser } from '@aws-lambda-powertools/parser/middleware';
* import middy from '@middy/core';
* import { sqsEnvelope } from '@aws-lambda-powertools/parser/envelopes/sqs;'
*
* const oderSchema = z.object({
* id: z.number(),
* description: z.string(),
* quantity: z.number(),
* });
*
* type Order = z.infer<typeof oderSchema>;
*
* export const handler = middy(
* async (event: Order, _context: unknown): Promise<void> => {
* // event is validated as sqs message envelope
* // the body is unwrapped and parsed into object ready to use
* // you can now use event as Order in your code
* }
* ).use(parser({ schema: oderSchema, envelope: sqsEnvelope }));
* ```
*
* @param options
*/
const parser = <S extends ZodSchema>(
options: ParserOptions<S>
): MiddlewareObj => {
const before = (request: MiddyLikeRequest): void => {
const { schema, envelope } = options;
if (envelope) {
request.event = envelope(request.event, schema);
} else {
request.event = schema.parse(request.event);
}
};

return {
before,
};
};

export { parser };
29 changes: 29 additions & 0 deletions packages/parser/src/types/envelope.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { type apiGatewayEnvelope } from '../envelopes/apigw.js';
import { type apiGatewayV2Envelope } from '../envelopes/apigwv2.js';
import { type cloudWatchEnvelope } from '../envelopes/cloudwatch.js';
import { type dynamoDDStreamEnvelope } from '../envelopes/dynamodb.js';
import { type kafkaEnvelope } from '../envelopes/kafka.js';
import { type kinesisEnvelope } from '../envelopes/kinesis.js';
import { type kinesisFirehoseEnvelope } from '../envelopes/kinesis-firehose.js';
import { type lambdaFunctionUrlEnvelope } from '../envelopes/lambda.js';
import { type snsEnvelope, type snsSqsEnvelope } from '../envelopes/sns.js';
import { type sqsEnvelope } from '../envelopes/sqs.js';
import { type vpcLatticeEnvelope } from '../envelopes/vpc-lattice.js';
import { type vpcLatticeV2Envelope } from '../envelopes/vpc-latticev2.js';
import { type eventBridgeEnvelope } from '../envelopes/event-bridge.js';

export type Envelope =
| typeof apiGatewayEnvelope
| typeof apiGatewayV2Envelope
| typeof cloudWatchEnvelope
| typeof dynamoDDStreamEnvelope
| typeof eventBridgeEnvelope
| typeof kafkaEnvelope
| typeof kinesisEnvelope
| typeof kinesisFirehoseEnvelope
| typeof lambdaFunctionUrlEnvelope
| typeof snsEnvelope
| typeof snsSqsEnvelope
| typeof sqsEnvelope
| typeof vpcLatticeEnvelope
| typeof vpcLatticeV2Envelope;
2 changes: 1 addition & 1 deletion packages/parser/tests/unit/envelopes/eventbridge.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import { TestEvents, TestSchema } from '../schema/utils.js';
import { generateMock } from '@anatine/zod-mock';
import { EventBridgeEvent } from 'aws-lambda';
import { eventBridgeEnvelope } from '../../../src/envelopes/eventbridge.js';
import { eventBridgeEnvelope } from '../../../src/envelopes/event-bridge.js';

describe('EventBridgeEnvelope ', () => {
it('should parse eventbridge event', () => {
Expand Down
123 changes: 123 additions & 0 deletions packages/parser/tests/unit/parser.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
/**
* Test middleware parser
*
* @group unit/parser
*/

import middy from '@middy/core';
import { Context } from 'aws-lambda';
import { parser } from '../../src/middleware/parser.js';
import { generateMock } from '@anatine/zod-mock';
import { SqsSchema } from '../../src/schemas/sqs.js';
import { z, type ZodSchema } from 'zod';
import { sqsEnvelope } from '../../src/envelopes/sqs';
import { TestSchema } from './schema/utils';

describe('Middleware: parser', () => {
type schema = z.infer<typeof TestSchema>;
const handler = async (
event: unknown,
_context: Context
): Promise<unknown> => {
return event;
};

describe(' when envelope is provided ', () => {
const middyfiedHandler = middy(handler).use(
parser({ schema: TestSchema, envelope: sqsEnvelope })
);

it('should parse request body with schema and envelope', async () => {
const bodyMock = generateMock(TestSchema);
parser({ schema: TestSchema, envelope: sqsEnvelope });

const event = generateMock(SqsSchema, {
stringMap: {
body: () => JSON.stringify(bodyMock),
},
});

const result = (await middyfiedHandler(event, {} as Context)) as schema[];
result.forEach((item) => {
expect(item).toEqual(bodyMock);
});
});

it('should throw when envelope does not match', async () => {
await expect(async () => {
await middyfiedHandler({ name: 'John', age: 18 }, {} as Context);
}).rejects.toThrowError();
});

it('should throw when schema does not match', async () => {
const event = generateMock(SqsSchema, {
stringMap: {
body: () => '42',
},
});

await expect(middyfiedHandler(event, {} as Context)).rejects.toThrow();
});
it('should throw when provided schema is invalid', async () => {
const middyfiedHandler = middy(handler).use(
parser({ schema: {} as ZodSchema, envelope: sqsEnvelope })
);

await expect(middyfiedHandler(42, {} as Context)).rejects.toThrowError();
});
it('should throw when envelope is correct but schema is invalid', async () => {
const event = generateMock(SqsSchema, {
stringMap: {
body: () => JSON.stringify({ name: 'John', foo: 'bar' }),
},
});

const middyfiedHandler = middy(handler).use(
parser({ schema: {} as ZodSchema, envelope: sqsEnvelope })
);

await expect(
middyfiedHandler(event, {} as Context)
).rejects.toThrowError();
});
});

describe(' when envelope is not provided', () => {
it('should parse the event with built-in schema', async () => {
const event = generateMock(SqsSchema);

const middyfiedHandler = middy(handler).use(
parser({ schema: SqsSchema })
);

expect(await middyfiedHandler(event, {} as Context)).toEqual(event);
});

it('should parse custom event', async () => {
const event = { name: 'John', age: 18 };
const middyfiedHandler = middy(handler).use(
parser({ schema: TestSchema })
);

expect(await middyfiedHandler(event, {} as Context)).toEqual(event);
});

it('should throw when the schema does not match', async () => {
const middyfiedHandler = middy(handler).use(
parser({ schema: TestSchema })
);

await expect(middyfiedHandler(42, {} as Context)).rejects.toThrow();
});

it('should throw when provided schema is invalid', async () => {
const middyfiedHandler = middy(handler).use(
parser({ schema: {} as ZodSchema })
);

await expect(
middyfiedHandler({ foo: 'bar' }, {} as Context)
).rejects.toThrowError();
});
});
});

0 comments on commit 803f8d9

Please sign in to comment.