diff --git a/packages/parser/src/envelopes/apigw.ts b/packages/parser/src/envelopes/apigw.ts index 49d094b405..ee4d71e9c0 100644 --- a/packages/parser/src/envelopes/apigw.ts +++ b/packages/parser/src/envelopes/apigw.ts @@ -1,18 +1,40 @@ -import { parse } from './envelope.js'; -import { z, ZodSchema } from 'zod'; +import { Envelope } from './envelope.js'; +import { z, type ZodSchema } from 'zod'; import { APIGatewayProxyEventSchema } from '../schemas/apigw.js'; +import type { ParsedResult } from '../types/parser.js'; /** * API Gateway envelope to extract data within body key */ -export const apiGatewayEnvelope = ( - data: unknown, - schema: T -): z.infer => { - const parsedEnvelope = APIGatewayProxyEventSchema.parse(data); - if (!parsedEnvelope.body) { - throw new Error('Body field of API Gateway event is undefined'); +export class ApiGatewayEnvelope extends Envelope { + public static parse( + data: unknown, + schema: T + ): z.infer { + return super.parse(APIGatewayProxyEventSchema.parse(data).body, schema); } - return parse(parsedEnvelope.body, schema); -}; + public static safeParse( + data: unknown, + schema: T + ): ParsedResult> { + const parsedEnvelope = APIGatewayProxyEventSchema.safeParse(data); + if (!parsedEnvelope.success) { + return { + ...parsedEnvelope, + originalEvent: data, + }; + } + + const parsedBody = super.safeParse(parsedEnvelope.data.body, schema); + + if (!parsedBody.success) { + return { + ...parsedBody, + originalEvent: data, + }; + } + + return parsedBody; + } +} diff --git a/packages/parser/src/envelopes/apigwv2.ts b/packages/parser/src/envelopes/apigwv2.ts index decadfcc57..3da86e26e4 100644 --- a/packages/parser/src/envelopes/apigwv2.ts +++ b/packages/parser/src/envelopes/apigwv2.ts @@ -1,18 +1,40 @@ -import { parse } from './envelope.js'; -import { z, ZodSchema } from 'zod'; +import { z, type ZodSchema } from 'zod'; import { APIGatewayProxyEventV2Schema } from '../schemas/apigwv2.js'; +import { Envelope } from './envelope.js'; +import type { ParsedResult } from '../types/index.js'; /** * API Gateway V2 envelope to extract data within body key */ -export const apiGatewayV2Envelope = ( - data: unknown, - schema: T -): z.infer => { - const parsedEnvelope = APIGatewayProxyEventV2Schema.parse(data); - if (!parsedEnvelope.body) { - throw new Error('Body field of API Gateway event is undefined'); +export class ApiGatewayV2Envelope extends Envelope { + public static parse( + data: unknown, + schema: T + ): z.infer { + return super.parse(APIGatewayProxyEventV2Schema.parse(data).body, schema); } - return parse(parsedEnvelope.body, schema); -}; + public static safeParse( + data: unknown, + schema: T + ): ParsedResult { + const parsedEnvelope = APIGatewayProxyEventV2Schema.safeParse(data); + if (!parsedEnvelope.success) { + return { + ...parsedEnvelope, + originalEvent: data, + }; + } + + const parsedBody = super.safeParse(parsedEnvelope.data.body, schema); + + if (!parsedBody.success) { + return { + ...parsedBody, + originalEvent: data, + }; + } + + return parsedBody; + } +} diff --git a/packages/parser/src/envelopes/cloudwatch.ts b/packages/parser/src/envelopes/cloudwatch.ts index 848e7ab070..f90e6455f9 100644 --- a/packages/parser/src/envelopes/cloudwatch.ts +++ b/packages/parser/src/envelopes/cloudwatch.ts @@ -1,6 +1,7 @@ -import { parse } from './envelope.js'; -import { z, ZodSchema } from 'zod'; -import { CloudWatchLogsSchema } from '../schemas/cloudwatch.js'; +import { z, type ZodSchema } from 'zod'; +import { Envelope } from './envelope.js'; +import { CloudWatchLogsSchema } from '../schemas/index.js'; +import type { ParsedResult } from '../types/index.js'; /** * CloudWatch Envelope to extract a List of log records. @@ -11,13 +12,49 @@ import { CloudWatchLogsSchema } from '../schemas/cloudwatch.js'; * * Note: The record will be parsed the same way so if model is str */ -export const cloudWatchEnvelope = ( - data: unknown, - schema: T -): z.infer => { - const parsedEnvelope = CloudWatchLogsSchema.parse(data); +export class CloudWatchEnvelope extends Envelope { + public static parse( + data: unknown, + schema: T + ): z.infer { + const parsedEnvelope = CloudWatchLogsSchema.parse(data); - return parsedEnvelope.awslogs.data.logEvents.map((record) => { - return parse(record.message, schema); - }); -}; + return parsedEnvelope.awslogs.data.logEvents.map((record) => { + return super.parse(record.message, schema); + }); + } + + public static safeParse( + data: unknown, + schema: T + ): ParsedResult { + const parsedEnvelope = CloudWatchLogsSchema.safeParse(data); + + if (!parsedEnvelope.success) { + return { + success: false, + error: parsedEnvelope.error, + originalEvent: data, + }; + } + const parsedLogEvents: z.infer[] = []; + + for (const record of parsedEnvelope.data.awslogs.data.logEvents) { + const parsedMessage = super.safeParse(record.message, schema); + if (!parsedMessage.success) { + return { + success: false, + error: parsedMessage.error, + originalEvent: data, + }; + } else { + parsedLogEvents.push(parsedMessage.data); + } + } + + return { + success: true, + data: parsedLogEvents, + }; + } +} diff --git a/packages/parser/src/envelopes/dynamodb.ts b/packages/parser/src/envelopes/dynamodb.ts index bb378b2d0b..4898369422 100644 --- a/packages/parser/src/envelopes/dynamodb.ts +++ b/packages/parser/src/envelopes/dynamodb.ts @@ -1,6 +1,7 @@ -import { parse } from './envelope.js'; -import { z, ZodSchema } from 'zod'; -import { DynamoDBStreamSchema } from '../schemas/dynamodb.js'; +import { z, type ZodSchema } from 'zod'; +import { DynamoDBStreamSchema } from '../schemas/index.js'; +import type { ParsedResult, ParsedResultError } from '../types/index.js'; +import { Envelope } from './envelope.js'; type DynamoDBStreamEnvelopeResponse = { NewImage: z.infer; @@ -13,16 +14,58 @@ type DynamoDBStreamEnvelopeResponse = { * Note: Values are the parsed models. Images' values can also be None, and * length of the list is the record's amount in the original event. */ -export const dynamoDDStreamEnvelope = ( - data: unknown, - schema: T -): DynamoDBStreamEnvelopeResponse[] => { - const parsedEnvelope = DynamoDBStreamSchema.parse(data); +export class DynamoDBStreamEnvelope extends Envelope { + public static parse( + data: unknown, + schema: T + ): DynamoDBStreamEnvelopeResponse>[] { + const parsedEnvelope = DynamoDBStreamSchema.parse(data); + + return parsedEnvelope.Records.map((record) => { + return { + NewImage: super.parse(record.dynamodb.NewImage, schema), + OldImage: super.parse(record.dynamodb.OldImage, schema), + }; + }); + } + + public static safeParse( + data: unknown, + schema: T + ): ParsedResult { + const parsedEnvelope = DynamoDBStreamSchema.safeParse(data); + + if (!parsedEnvelope.success) { + return { + success: false, + error: parsedEnvelope.error, + originalEvent: data, + }; + } + const parsedLogEvents: DynamoDBStreamEnvelopeResponse>[] = []; + + for (const record of parsedEnvelope.data.Records) { + const parsedNewImage = super.safeParse(record.dynamodb.NewImage, schema); + const parsedOldImage = super.safeParse(record.dynamodb.OldImage, schema); + if (!parsedNewImage.success || !parsedOldImage.success) { + return { + success: false, + error: !parsedNewImage.success + ? parsedNewImage.error + : (parsedOldImage as ParsedResultError).error, + originalEvent: data, + }; + } else { + parsedLogEvents.push({ + NewImage: parsedNewImage.data, + OldImage: parsedOldImage.data, + }); + } + } - return parsedEnvelope.Records.map((record) => { return { - NewImage: parse(record.dynamodb.NewImage, schema), - OldImage: parse(record.dynamodb.OldImage, schema), + success: true, + data: parsedLogEvents, }; - }); -}; + } +} diff --git a/packages/parser/src/envelopes/envelope.ts b/packages/parser/src/envelopes/envelope.ts index 4c2dd9570d..ed55758153 100644 --- a/packages/parser/src/envelopes/envelope.ts +++ b/packages/parser/src/envelopes/envelope.ts @@ -1,23 +1,70 @@ -import { z, ZodSchema } from 'zod'; +import { z, type ZodSchema } from 'zod'; +import type { ParsedResult } from '../types/parser.js'; -/** - * Abstract function to parse the content of the envelope using provided schema. - * Both inputs are provided as unknown by the user. - * We expect the data to be either string that can be parsed to json or object. - * @internal - * @param data data to parse - * @param schema schema - */ -export const parse = ( - data: unknown, - schema: T -): z.infer[] => { - if (typeof data === 'string') { - return schema.parse(JSON.parse(data)); - } else if (typeof data === 'object') { - return schema.parse(data); - } else - throw new Error( - `Invalid data type for envelope. Expected string or object, got ${typeof data}` - ); -}; +export class Envelope { + /** + * Abstract function to parse the content of the envelope using provided schema. + * Both inputs are provided as unknown by the user. + * We expect the data to be either string that can be parsed to json or object. + * @internal + * @param data data to parse + * @param schema schema + */ + public static readonly parse = ( + data: unknown, + schema: T + ): z.infer => { + if (typeof data === 'string') { + return schema.parse(JSON.parse(data)); + } else if (typeof data === 'object') { + return schema.parse(data); + } else + throw new Error( + `Invalid data type for envelope. Expected string or object, got ${typeof data}` + ); + }; + + /** + * Abstract function to safely parse the content of the envelope using provided schema. + * safeParse is used to avoid throwing errors, thus we catuch all errors and wrap them in the result. + * @param input + * @param schema + */ + public static readonly safeParse = ( + input: unknown, + schema: T + ): ParsedResult> => { + try { + if (typeof input !== 'object' && typeof input !== 'string') { + return { + success: false, + error: new Error( + `Invalid data type for envelope. Expected string or object, got ${typeof input}` + ), + originalEvent: input, + }; + } + + const parsed = schema.safeParse( + typeof input === 'string' ? JSON.parse(input) : input + ); + + return parsed.success + ? { + success: true, + data: parsed.data, + } + : { + success: false, + error: parsed.error, + originalEvent: input, + }; + } catch (e) { + return { + success: false, + error: e as Error, + originalEvent: input, + }; + } + }; +} diff --git a/packages/parser/src/envelopes/event-bridge.ts b/packages/parser/src/envelopes/event-bridge.ts index 4484635348..0d166d673b 100644 --- a/packages/parser/src/envelopes/event-bridge.ts +++ b/packages/parser/src/envelopes/event-bridge.ts @@ -1,13 +1,41 @@ -import { parse } from './envelope.js'; -import { z, ZodSchema } from 'zod'; -import { EventBridgeSchema } from '../schemas/eventbridge.js'; +import { Envelope } from './envelope.js'; +import { z, type ZodSchema } from 'zod'; +import { EventBridgeSchema } from '../schemas/index.js'; +import type { ParsedResult } from '../types/index.js'; /** * Envelope for EventBridge schema that extracts and parses data from the `detail` key. */ -export const eventBridgeEnvelope = ( - data: unknown, - schema: T -): z.infer => { - return parse(EventBridgeSchema.parse(data).detail, schema); -}; +export class EventBridgeEnvelope extends Envelope { + public static parse( + data: unknown, + schema: T + ): z.infer { + return super.parse(EventBridgeSchema.parse(data).detail, schema); + } + + public static safeParse( + data: unknown, + schema: T + ): ParsedResult { + const parsedEnvelope = EventBridgeSchema.safeParse(data); + + if (!parsedEnvelope.success) { + return { + ...parsedEnvelope, + originalEvent: data, + }; + } + + const parsedDetail = super.safeParse(parsedEnvelope.data.detail, schema); + + if (!parsedDetail.success) { + return { + ...parsedDetail, + originalEvent: data, + }; + } + + return parsedDetail; + } +} diff --git a/packages/parser/src/envelopes/index.ts b/packages/parser/src/envelopes/index.ts index dd28c4aeea..3d1487a7b0 100644 --- a/packages/parser/src/envelopes/index.ts +++ b/packages/parser/src/envelopes/index.ts @@ -1,13 +1,13 @@ -export { apiGatewayEnvelope } from './apigw.js'; -export { apiGatewayV2Envelope } from './apigwv2.js'; -export { cloudWatchEnvelope } from './cloudwatch.js'; -export { dynamoDDStreamEnvelope } from './dynamodb.js'; -export { eventBridgeEnvelope } from './event-bridge.js'; -export { kafkaEnvelope } from './kafka.js'; -export { kinesisEnvelope } from './kinesis.js'; -export { kinesisFirehoseEnvelope } from './kinesis-firehose.js'; -export { lambdaFunctionUrlEnvelope } from './lambda.js'; -export { snsEnvelope, snsSqsEnvelope } from './sns.js'; -export { sqsEnvelope } from './sqs.js'; -export { vpcLatticeEnvelope } from './vpc-lattice.js'; -export { vpcLatticeV2Envelope } from './vpc-latticev2.js'; +export { ApiGatewayEnvelope } from './apigw.js'; +export { ApiGatewayV2Envelope } from './apigwv2.js'; +export { CloudWatchEnvelope } from './cloudwatch.js'; +export { DynamoDBStreamEnvelope } from './dynamodb.js'; +export { EventBridgeEnvelope } from './event-bridge.js'; +export { KafkaEnvelope } from './kafka.js'; +export { KinesisEnvelope } from './kinesis.js'; +export { KinesisFirehoseEnvelope } from './kinesis-firehose.js'; +export { LambdaFunctionUrlEnvelope } from './lambda.js'; +export { SnsEnvelope, SnsSqsEnvelope } from './sns.js'; +export { SqsEnvelope } from './sqs.js'; +export { VpcLatticeEnvelope } from './vpc-lattice.js'; +export { VpcLatticeV2Envelope } from './vpc-latticev2.js'; diff --git a/packages/parser/src/envelopes/kafka.ts b/packages/parser/src/envelopes/kafka.ts index 86eb44063f..1bbd442f3f 100644 --- a/packages/parser/src/envelopes/kafka.ts +++ b/packages/parser/src/envelopes/kafka.ts @@ -1,9 +1,10 @@ -import { z, ZodSchema } from 'zod'; -import { parse } from './envelope.js'; +import { z, type ZodSchema } from 'zod'; +import { Envelope } from './envelope.js'; import { KafkaMskEventSchema, KafkaSelfManagedEventSchema, } from '../schemas/kafka.js'; +import { ParsedResult, KafkaMskEvent } from '../types/index.js'; /** * Kafka event envelope to extract data within body key @@ -13,26 +14,66 @@ import { * Note: Records will be parsed the same way so if model is str, * all items in the list will be parsed as str and not as JSON (and vice versa) */ -export const kafkaEnvelope = ( - data: unknown, - schema: T -): z.infer => { - // manually fetch event source to deside between Msk or SelfManaged - - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - const eventSource = data['eventSource']; - - const parsedEnvelope: - | z.infer - | z.infer = - eventSource === 'aws:kafka' - ? KafkaMskEventSchema.parse(data) - : KafkaSelfManagedEventSchema.parse(data); - - return Object.values(parsedEnvelope.records).map((topicRecord) => { - return topicRecord.map((record) => { - return parse(record.value, schema); + +export class KafkaEnvelope extends Envelope { + public static parse( + data: unknown, + schema: T + ): z.infer { + // manually fetch event source to deside between Msk or SelfManaged + const eventSource = (data as KafkaMskEvent)['eventSource']; + + const parsedEnvelope: + | z.infer + | z.infer = + eventSource === 'aws:kafka' + ? KafkaMskEventSchema.parse(data) + : KafkaSelfManagedEventSchema.parse(data); + + return Object.values(parsedEnvelope.records).map((topicRecord) => { + return topicRecord.map((record) => { + return super.parse(record.value, schema); + }); }); - }); -}; + } + + public static safeParse( + data: unknown, + schema: T + ): ParsedResult { + // manually fetch event source to deside between Msk or SelfManaged + const eventSource = (data as KafkaMskEvent)['eventSource']; + + const parsedEnvelope = + eventSource === 'aws:kafka' + ? KafkaMskEventSchema.safeParse(data) + : KafkaSelfManagedEventSchema.safeParse(data); + + if (!parsedEnvelope.success) { + return { + ...parsedEnvelope, + originalEvent: data, + }; + } + const parsedRecords: z.infer[] = []; + + for (const topicRecord of Object.values(parsedEnvelope.data.records)) { + for (const record of topicRecord) { + const parsedRecord = super.safeParse(record.value, schema); + if (!parsedRecord.success) { + return { + success: false, + error: parsedRecord.error, + originalEvent: data, + }; + } + parsedRecords.push(parsedRecord.data); + } + } + + return { + success: true, + data: parsedRecords, + }; + } +} diff --git a/packages/parser/src/envelopes/kinesis-firehose.ts b/packages/parser/src/envelopes/kinesis-firehose.ts index e51ae47a12..7dca3cae73 100644 --- a/packages/parser/src/envelopes/kinesis-firehose.ts +++ b/packages/parser/src/envelopes/kinesis-firehose.ts @@ -1,6 +1,7 @@ -import { parse } from './envelope.js'; -import { z, ZodSchema } from 'zod'; -import { KinesisFirehoseSchema } from '../schemas/kinesis-firehose.js'; +import { z, type ZodSchema } from 'zod'; +import { Envelope } from './envelope.js'; +import { KinesisFirehoseSchema } from '../schemas/index.js'; +import type { ParsedResult } from '../types/index.js'; /** * Kinesis Firehose Envelope to extract array of Records @@ -14,13 +15,47 @@ import { KinesisFirehoseSchema } from '../schemas/kinesis-firehose.js'; * * https://docs.aws.amazon.com/lambda/latest/dg/services-kinesisfirehose.html */ -export const kinesisFirehoseEnvelope = ( - data: unknown, - schema: T -): z.infer => { - const parsedEnvelope = KinesisFirehoseSchema.parse(data); +export class KinesisFirehoseEnvelope extends Envelope { + public static parse( + data: unknown, + schema: T + ): z.infer { + const parsedEnvelope = KinesisFirehoseSchema.parse(data); - return parsedEnvelope.records.map((record) => { - return parse(record.data, schema); - }); -}; + return parsedEnvelope.records.map((record) => { + return super.parse(record.data, schema); + }); + } + + public static safeParse( + data: unknown, + schema: T + ): ParsedResult { + const parsedEnvelope = KinesisFirehoseSchema.safeParse(data); + + if (!parsedEnvelope.success) { + return { + ...parsedEnvelope, + originalEvent: data, + }; + } + const parsedRecords: z.infer[] = []; + + for (const record of parsedEnvelope.data.records) { + const parsedData = super.safeParse(record.data, schema); + if (!parsedData.success) { + return { + success: false, + error: parsedData.error, + originalEvent: data, + }; + } + parsedRecords.push(parsedData.data); + } + + return { + success: true, + data: parsedRecords, + }; + } +} diff --git a/packages/parser/src/envelopes/kinesis.ts b/packages/parser/src/envelopes/kinesis.ts index 311223042d..470df40522 100644 --- a/packages/parser/src/envelopes/kinesis.ts +++ b/packages/parser/src/envelopes/kinesis.ts @@ -1,6 +1,7 @@ -import { parse } from './envelope.js'; -import { z, ZodSchema } from 'zod'; +import { Envelope } from './envelope.js'; +import { z, type ZodSchema } from 'zod'; import { KinesisDataStreamSchema } from '../schemas/kinesis.js'; +import type { ParsedResult } from '../types/index.js'; /** * Kinesis Data Stream Envelope to extract array of Records @@ -12,13 +13,46 @@ import { KinesisDataStreamSchema } from '../schemas/kinesis.js'; * Note: Records will be parsed the same way so if model is str, * all items in the list will be parsed as str and not as JSON (and vice versa) */ -export const kinesisEnvelope = ( - data: unknown, - schema: T -): z.infer => { - const parsedEnvelope = KinesisDataStreamSchema.parse(data); +export class KinesisEnvelope extends Envelope { + public static parse( + data: unknown, + schema: T + ): z.infer { + const parsedEnvelope = KinesisDataStreamSchema.parse(data); - return parsedEnvelope.Records.map((record) => { - return parse(record.kinesis.data, schema); - }); -}; + return parsedEnvelope.Records.map((record) => { + return super.parse(record.kinesis.data, schema); + }); + } + + public static safeParse( + data: unknown, + schema: T + ): ParsedResult { + const parsedEnvelope = KinesisDataStreamSchema.safeParse(data); + if (!parsedEnvelope.success) { + return { + ...parsedEnvelope, + originalEvent: data, + }; + } + + const parsedRecords: z.infer[] = []; + + for (const record of parsedEnvelope.data.Records) { + const parsedRecord = super.safeParse(record.kinesis.data, schema); + if (!parsedRecord.success) { + return { + ...parsedRecord, + originalEvent: data, + }; + } + parsedRecords.push(parsedRecord.data); + } + + return { + success: true, + data: parsedRecords, + }; + } +} diff --git a/packages/parser/src/envelopes/lambda.ts b/packages/parser/src/envelopes/lambda.ts index 3ac1f2b8c6..a839c68287 100644 --- a/packages/parser/src/envelopes/lambda.ts +++ b/packages/parser/src/envelopes/lambda.ts @@ -1,18 +1,46 @@ -import { parse } from './envelope.js'; -import { z, ZodSchema } from 'zod'; -import { LambdaFunctionUrlSchema } from '../schemas/lambda.js'; +import { Envelope } from './envelope.js'; +import { z, type ZodSchema } from 'zod'; +import { LambdaFunctionUrlSchema } from '../schemas/index.js'; +import type { ParsedResult } from '../types/index.js'; /** * Lambda function URL envelope to extract data within body key */ -export const lambdaFunctionUrlEnvelope = ( - data: unknown, - schema: T -): z.infer => { - const parsedEnvelope = LambdaFunctionUrlSchema.parse(data); - if (!parsedEnvelope.body) { - throw new Error('Body field of Lambda function URL event is undefined'); +export class LambdaFunctionUrlEnvelope extends Envelope { + public static parse( + data: unknown, + schema: T + ): z.infer { + const parsedEnvelope = LambdaFunctionUrlSchema.parse(data); + + if (!parsedEnvelope.body) { + throw new Error('Body field of Lambda function URL event is undefined'); + } + + return super.parse(parsedEnvelope.body, schema); } - return parse(parsedEnvelope.body, schema); -}; + public static safeParse( + data: unknown, + schema: T + ): ParsedResult { + const parsedEnvelope = LambdaFunctionUrlSchema.safeParse(data); + + if (!parsedEnvelope.success) { + return { + ...parsedEnvelope, + originalEvent: data, + }; + } + + const parsedBody = super.safeParse(parsedEnvelope.data.body, schema); + if (!parsedBody.success) { + return { + ...parsedBody, + originalEvent: data, + }; + } + + return parsedBody; + } +} diff --git a/packages/parser/src/envelopes/sns.ts b/packages/parser/src/envelopes/sns.ts index 3e897a00a8..1141dff305 100644 --- a/packages/parser/src/envelopes/sns.ts +++ b/packages/parser/src/envelopes/sns.ts @@ -1,7 +1,8 @@ -import { z, ZodSchema } from 'zod'; -import { parse } from './envelope.js'; +import { z, type ZodSchema } from 'zod'; +import { Envelope } from './envelope.js'; import { SnsSchema, SnsSqsNotificationSchema } from '../schemas/sns.js'; import { SqsSchema } from '../schemas/sqs.js'; +import type { ParsedResult } from '../types/index.js'; /** * SNS Envelope to extract array of Records @@ -12,16 +13,49 @@ import { SqsSchema } from '../schemas/sqs.js'; * Note: Records will be parsed the same way so if model is str, * all items in the list will be parsed as str and npt as JSON (and vice versa) */ -export const snsEnvelope = ( - data: unknown, - schema: T -): z.infer => { - const parsedEnvelope = SnsSchema.parse(data); +export class SnsEnvelope extends Envelope { + public static parse( + data: unknown, + schema: T + ): z.infer { + const parsedEnvelope = SnsSchema.parse(data); - return parsedEnvelope.Records.map((record) => { - return parse(record.Sns.Message, schema); - }); -}; + return parsedEnvelope.Records.map((record) => { + return super.parse(record.Sns.Message, schema); + }); + } + + public static safeParse( + data: unknown, + schema: T + ): ParsedResult { + const parsedEnvelope = SnsSchema.safeParse(data); + + if (!parsedEnvelope.success) { + return { + ...parsedEnvelope, + originalEvent: data, + }; + } + + const parsedMessages: z.infer[] = []; + for (const record of parsedEnvelope.data.Records) { + const parsedMessage = super.safeParse(record.Sns.Message, schema); + if (!parsedMessage.success) { + return { + ...parsedMessage, + originalEvent: data, + }; + } + parsedMessages.push(parsedMessage.data); + } + + return { + success: true, + data: parsedMessages, + }; + } +} /** * SNS plus SQS Envelope to extract array of Records @@ -34,17 +68,68 @@ export const snsEnvelope = ( * 3. Finally, parse provided model against payload extracted * */ -export const snsSqsEnvelope = ( - data: unknown, - schema: T -): z.infer => { - const parsedEnvelope = SqsSchema.parse(data); - - return parsedEnvelope.Records.map((record) => { - const snsNotification = SnsSqsNotificationSchema.parse( - JSON.parse(record.body) - ); - - return parse(snsNotification.Message, schema); - }); -}; +export class SnsSqsEnvelope extends Envelope { + public static parse( + data: unknown, + schema: T + ): z.infer { + const parsedEnvelope = SqsSchema.parse(data); + + return parsedEnvelope.Records.map((record) => { + const snsNotification = SnsSqsNotificationSchema.parse( + JSON.parse(record.body) + ); + + return super.parse(snsNotification.Message, schema); + }); + } + + public static safeParse( + data: unknown, + schema: T + ): ParsedResult { + const parsedEnvelope = SqsSchema.safeParse(data); + if (!parsedEnvelope.success) { + return { + ...parsedEnvelope, + originalEvent: data, + }; + } + + const parsedMessages: z.infer[] = []; + + // JSON.parse can throw an error, thus we catch it and return ParsedErrorResult + try { + for (const record of parsedEnvelope.data.Records) { + const snsNotification = SnsSqsNotificationSchema.safeParse( + JSON.parse(record.body) + ); + if (!snsNotification.success) { + return { + ...snsNotification, + originalEvent: data, + }; + } + const parsedMessage = super.safeParse( + snsNotification.data.Message, + schema + ); + if (!parsedMessage.success) { + return { + ...parsedMessage, + originalEvent: data, + }; + } + parsedMessages.push(parsedMessage.data); + } + } catch (e) { + return { + success: false, + error: e as Error, + originalEvent: data, + }; + } + + return { success: true, data: parsedMessages }; + } +} diff --git a/packages/parser/src/envelopes/sqs.ts b/packages/parser/src/envelopes/sqs.ts index 2757663a95..b1c8f6c059 100644 --- a/packages/parser/src/envelopes/sqs.ts +++ b/packages/parser/src/envelopes/sqs.ts @@ -1,6 +1,7 @@ -import { z, ZodSchema } from 'zod'; +import { z, type ZodSchema } from 'zod'; import { SqsSchema } from '../schemas/sqs.js'; -import { parse } from './envelope.js'; +import { Envelope } from './envelope.js'; +import type { ParsedResult } from '../types/index.js'; /** * SQS Envelope to extract array of Records @@ -11,13 +12,42 @@ import { parse } from './envelope.js'; * Note: Records will be parsed the same way so if model is str, * all items in the list will be parsed as str and npt as JSON (and vice versa) */ -export const sqsEnvelope = ( - data: unknown, - schema: T -): z.infer => { - const parsedEnvelope = SqsSchema.parse(data); +export class SqsEnvelope extends Envelope { + public static parse( + data: unknown, + schema: T + ): z.infer { + const parsedEnvelope = SqsSchema.parse(data); - return parsedEnvelope.Records.map((record) => { - return parse(record.body, schema); - }); -}; + return parsedEnvelope.Records.map((record) => { + return super.parse(record.body, schema); + }); + } + + public static safeParse( + data: unknown, + schema: T + ): ParsedResult { + const parsedEnvelope = SqsSchema.safeParse(data); + if (!parsedEnvelope.success) { + return { + ...parsedEnvelope, + originalEvent: data, + }; + } + + const parsedRecords: z.infer[] = []; + for (const record of parsedEnvelope.data.Records) { + const parsedRecord = super.safeParse(record.body, schema); + if (!parsedRecord.success) { + return { + ...parsedRecord, + originalEvent: data, + }; + } + parsedRecords.push(parsedRecord.data); + } + + return { success: true, data: parsedRecords }; + } +} diff --git a/packages/parser/src/envelopes/vpc-lattice.ts b/packages/parser/src/envelopes/vpc-lattice.ts index 03d2998757..a7150dd5a4 100644 --- a/packages/parser/src/envelopes/vpc-lattice.ts +++ b/packages/parser/src/envelopes/vpc-lattice.ts @@ -1,15 +1,43 @@ -import { parse } from './envelope.js'; -import { z, ZodSchema } from 'zod'; -import { VpcLatticeSchema } from '../schemas/vpc-lattice.js'; +import { z, type ZodSchema } from 'zod'; +import { Envelope } from './envelope.js'; +import { VpcLatticeSchema } from '../schemas/index.js'; +import type { ParsedResult } from '../types/index.js'; /** * Amazon VPC Lattice envelope to extract data within body key */ -export const vpcLatticeEnvelope = ( - data: unknown, - schema: T -): z.infer => { - const parsedEnvelope = VpcLatticeSchema.parse(data); - - return parse(parsedEnvelope.body, schema); -}; + +export class VpcLatticeEnvelope extends Envelope { + public static parse( + data: unknown, + schema: T + ): z.infer { + const parsedEnvelope = VpcLatticeSchema.parse(data); + + return super.parse(parsedEnvelope.body, schema); + } + + public static safeParse( + data: unknown, + schema: T + ): ParsedResult { + const parsedEnvelope = VpcLatticeSchema.safeParse(data); + if (!parsedEnvelope.success) { + return { + ...parsedEnvelope, + originalEvent: data, + }; + } + + const parsedBody = super.safeParse(parsedEnvelope.data.body, schema); + + if (!parsedBody.success) { + return { + ...parsedBody, + originalEvent: data, + }; + } + + return parsedBody; + } +} diff --git a/packages/parser/src/envelopes/vpc-latticev2.ts b/packages/parser/src/envelopes/vpc-latticev2.ts index a3fa4389c0..7ef31b0702 100644 --- a/packages/parser/src/envelopes/vpc-latticev2.ts +++ b/packages/parser/src/envelopes/vpc-latticev2.ts @@ -1,15 +1,42 @@ -import { parse } from './envelope.js'; -import { z, ZodSchema } from 'zod'; -import { VpcLatticeV2Schema } from '../schemas/vpc-latticev2.js'; +import { Envelope } from './envelope.js'; +import { z, type ZodSchema } from 'zod'; +import { VpcLatticeV2Schema } from '../schemas/index.js'; +import type { ParsedResult } from '../types/index.js'; /** * Amazon VPC Lattice envelope to extract data within body key */ -export const vpcLatticeV2Envelope = ( - data: unknown, - schema: T -): z.infer => { - const parsedEnvelope = VpcLatticeV2Schema.parse(data); - - return parse(parsedEnvelope.body, schema); -}; +export class VpcLatticeV2Envelope extends Envelope { + public static parse( + data: unknown, + schema: T + ): z.infer { + const parsedEnvelope = VpcLatticeV2Schema.parse(data); + + return super.parse(parsedEnvelope.body, schema); + } + + public static safeParse( + data: unknown, + schema: T + ): ParsedResult { + const parsedEnvelope = VpcLatticeV2Schema.safeParse(data); + if (!parsedEnvelope.success) { + return { + ...parsedEnvelope, + originalEvent: data, + }; + } + + const parsedBody = super.safeParse(parsedEnvelope.data.body, schema); + + if (!parsedBody.success) { + return { + ...parsedBody, + originalEvent: data, + }; + } + + return parsedBody; + } +} diff --git a/packages/parser/src/index.ts b/packages/parser/src/index.ts index bb7ee75eb5..940216fe90 100644 --- a/packages/parser/src/index.ts +++ b/packages/parser/src/index.ts @@ -1 +1 @@ -export { parser } from './parser.js'; +export { parser } from './parserDecorator.js'; diff --git a/packages/parser/src/middleware/parser.ts b/packages/parser/src/middleware/parser.ts index 8a2c26c327..0c536b0aeb 100644 --- a/packages/parser/src/middleware/parser.ts +++ b/packages/parser/src/middleware/parser.ts @@ -1,7 +1,8 @@ import { type MiddyLikeRequest } from '@aws-lambda-powertools/commons/types'; import { type MiddlewareObj } from '@middy/core'; import { type ZodSchema } from 'zod'; -import { type ParserOptions } from '../types/ParserOptions.js'; +import { type ParserOptions } from '../types/parser.js'; +import { parse } from '../parser.js'; /** * A middiy middleware to parse your event. @@ -35,12 +36,9 @@ const parser = ( options: ParserOptions ): 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); - } + const { schema, envelope, safeParse } = options; + + request.event = parse(request.event, envelope, schema, safeParse); }; return { diff --git a/packages/parser/src/parser.ts b/packages/parser/src/parser.ts index 1320b643a8..89ed2e4ffb 100644 --- a/packages/parser/src/parser.ts +++ b/packages/parser/src/parser.ts @@ -1,57 +1,66 @@ -import { HandlerMethodDecorator } from '@aws-lambda-powertools/commons/types'; -import { Context, Handler } from 'aws-lambda'; -import { ZodSchema } from 'zod'; -import { type ParserOptions } from './types/ParserOptions.js'; +import type { ParsedResult, Envelope } from './types/index.js'; +import { z, type ZodSchema } from 'zod'; /** - * A decorator to parse your event. + * Parse the data using the provided schema, envelope and safeParse flag * * @example * ```typescript - * - * import { parser } from '@aws-lambda-powertools/parser'; - * import { sqsEnvelope } from '@aws-lambda-powertools/parser/envelopes/sqs'; - * + * import { z } from 'zod'; + * import type { SqsEvent, ParsedResult } from '@aws-lambda-powertools/parser/types'; + * import { SqsEnvelope } from '@aws-lambda-powertools/parser/types/envelopes'; + * import { parse } from '@aws-lambda-powertools/parser'; * * const Order = z.object({ - * orderId: z.string(), - * description: z.string(), - * } + * orderId: z.string(), + * description: z.string(), + * }); * - * class Lambda extends LambdaInterface { + * const handler = async (event: SqsEvent, context: unknown): Promise => { + * const parsedEvent = parse(event, SqsEnvelope, Order); * - * @parser({ envelope: sqsEnvelope, schema: OrderSchema }) - * public async handler(event: Order, _context: Context): Promise { - * // sqs event is parsed and the payload is extracted and parsed - * // apply business logic to your Order event - * const res = processOrder(event); - * return res; - * } + * const parsedSafe: ParsedResult = parse(event, SqsEnvelope, Order, true) * } - * - * @param options + * @param data the data to parse + * @param envelope the envelope to use, can be undefined + * @param schema the schema to use + * @param safeParse whether to use safeParse or not, if true it will return a ParsedResult with the original event if the parsing fails */ -export const parser = ( - options: ParserOptions -): HandlerMethodDecorator => { - return (_target, _propertyKey, descriptor) => { - const original = descriptor.value!; - - const { schema, envelope } = options; +const parse = ( + data: z.infer, + envelope: E | undefined, + schema: T, + safeParse?: boolean +): ParsedResult | z.infer => { + if (envelope && safeParse) { + return envelope.safeParse(data, schema); + } + if (envelope) { + return envelope.parse(data, schema); + } + if (safeParse) { + return safeParseSchema(data, schema); + } - descriptor.value = async function ( - this: Handler, - event: unknown, - context: Context, - callback - ) { - const parsedEvent = envelope - ? envelope(event, schema) - : schema.parse(event); + return schema.parse(data); +}; - return original.call(this, parsedEvent, context, callback); - }; +/** + * Parse the data safely using the provided schema. + * This function will not throw an error if the parsing fails, instead it will return a ParsedResultError with the original event. + * Otherwise, it will return ParsedResultSuccess with the parsed data. + * @param data the data to parse + * @param schema the zod schema to use + */ +const safeParseSchema = ( + data: z.infer, + schema: T +): ParsedResult => { + const result = schema.safeParse(data); - return descriptor; - }; + return result.success + ? result + : { success: false, error: result.error, originalEvent: data }; }; + +export { parse }; diff --git a/packages/parser/src/parserDecorator.ts b/packages/parser/src/parserDecorator.ts new file mode 100644 index 0000000000..0572d3a22e --- /dev/null +++ b/packages/parser/src/parserDecorator.ts @@ -0,0 +1,94 @@ +import type { HandlerMethodDecorator } from '@aws-lambda-powertools/commons/types'; +import type { Context, Handler } from 'aws-lambda'; +import { ZodSchema, z } from 'zod'; +import { parse } from './parser.js'; +import type { ParserOptions, ParsedResult } from './types/index.js'; + +/** + * A decorator to parse your event. + * + * @example + * ```typescript + * import { z } from 'zod'; + * import type { LambdaInterface } from '@aws-lambda-powertools/commons/types'; + * import type { SqSEvent } from '@aws-lambda-powertools/parser/types; + * import { parser } from '@aws-lambda-powertools/parser'; + * import { SqsEnvelope } from '@aws-lambda-powertools/parser/envelopes'; + * + * const Order = z.object({ + * orderId: z.string(), + * description: z.string(), + * }); + * + * class Lambda implements LambdaInterface { + * + * ⁣@parser({ envelope: SqsEnvelope, schema: OrderSchema }) + * public async handler(event: Order, _context: Context): Promise { + * // sqs event is parsed and the payload is extracted and parsed + * // apply business logic to your Order event + * const res = processOrder(event); + * return res; + * } + * } + * + * ``` + * + * In case you want to parse the event and handle the error, you can use the safeParse option. + * The safeParse option will return an object with the parsed event and an error object if the parsing fails. + * + * @example + * ```typescript + * + * import type { LambdaInterface } from '@aws-lambda-powertools/commons/types'; + * import type { SqSEvent, ParsedResult } from '@aws-lambda-powertools/parser/types; + * import { z } from 'zod'; + * import { parser } from '@aws-lambda-powertools/parser'; + * import { SqsEnvelope } from '@aws-lambda-powertools/parser/envelopes'; + * + * + * const Order = z.object({ + * orderId: z.string(), + * description: z.string(), + * } + * + * class Lambda implements LambdaInterface { + * + * ⁣git@parser({ envelope: SqsEnvelope, schema: OrderSchema, safeParse: true }) + * public async handler(event: ParsedResult, _context: unknown): Promise { + * if (event.success) { + * // event.data is the parsed event object of type Order + * } else { + * // event.error is the error object, you can inspect and recover + * // event.originalEvent is the original event that failed to parse + * } + * } + * } + * ``` + * + * @param options Configure the parser with the `schema`, `envelope` and whether to `safeParse` or not + */ +export const parser = ( + options: ParserOptions +): HandlerMethodDecorator => { + return (_target, _propertyKey, descriptor) => { + const original = descriptor.value!; + + const { schema, envelope, safeParse } = options; + + descriptor.value = async function ( + this: Handler, + event: unknown, + context: Context, + callback + ) { + const parsedEvent: ParsedResult< + typeof event, + z.infer + > = parse(event, envelope, schema, safeParse); + + return original.call(this, parsedEvent, context, callback); + }; + + return descriptor; + }; +}; diff --git a/packages/parser/src/types/ParserOptions.ts b/packages/parser/src/types/ParserOptions.ts deleted file mode 100644 index 57fcb9beca..0000000000 --- a/packages/parser/src/types/ParserOptions.ts +++ /dev/null @@ -1,7 +0,0 @@ -import type { ZodSchema } from 'zod'; -import { Envelope } from './envelope.js'; - -export type ParserOptions = { - schema: S; - envelope?: Envelope; -}; diff --git a/packages/parser/src/types/envelope.ts b/packages/parser/src/types/envelope.ts index e54958dca2..9e049c1178 100644 --- a/packages/parser/src/types/envelope.ts +++ b/packages/parser/src/types/envelope.ts @@ -1,29 +1,32 @@ -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'; +import type { + ApiGatewayEnvelope, + KinesisFirehoseEnvelope, + KinesisEnvelope, + KafkaEnvelope, + CloudWatchEnvelope, + EventBridgeEnvelope, + ApiGatewayV2Envelope, + DynamoDBStreamEnvelope, + LambdaFunctionUrlEnvelope, + SnsEnvelope, + SnsSqsEnvelope, + SqsEnvelope, + VpcLatticeEnvelope, + VpcLatticeV2Envelope, +} from '../envelopes/index.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; + | typeof ApiGatewayEnvelope + | typeof ApiGatewayV2Envelope + | typeof CloudWatchEnvelope + | typeof DynamoDBStreamEnvelope + | typeof EventBridgeEnvelope + | typeof KafkaEnvelope + | typeof KinesisEnvelope + | typeof KinesisFirehoseEnvelope + | typeof LambdaFunctionUrlEnvelope + | typeof SnsEnvelope + | typeof SnsSqsEnvelope + | typeof SqsEnvelope + | typeof VpcLatticeEnvelope + | typeof VpcLatticeV2Envelope; diff --git a/packages/parser/src/types/index.ts b/packages/parser/src/types/index.ts index d1c8e6fc13..26052e60b6 100644 --- a/packages/parser/src/types/index.ts +++ b/packages/parser/src/types/index.ts @@ -1,4 +1,9 @@ -export type { ParserOptions } from './ParserOptions.js'; +export type { + ParserOptions, + ParsedResult, + ParsedResultSuccess, + ParsedResultError, +} from '../types/parser.js'; export type { Envelope } from './envelope.js'; export type { diff --git a/packages/parser/src/types/parser.ts b/packages/parser/src/types/parser.ts new file mode 100644 index 0000000000..c775a78d94 --- /dev/null +++ b/packages/parser/src/types/parser.ts @@ -0,0 +1,30 @@ +import type { ZodSchema, ZodError } from 'zod'; +import type { Envelope } from './envelope.js'; + +type ParserOptions = { + schema: S; + envelope?: Envelope; + safeParse?: boolean; +}; + +type ParsedResultSuccess = { + success: true; + data: Output; +}; + +type ParsedResultError = { + success: false; + error: ZodError | Error; + originalEvent: Input; +}; + +type ParsedResult = + | ParsedResultSuccess + | ParsedResultError; + +export type { + ParserOptions, + ParsedResult, + ParsedResultError, + ParsedResultSuccess, +}; diff --git a/packages/parser/src/types/schema.ts b/packages/parser/src/types/schema.ts index 6e15f17bd0..6b18b57968 100644 --- a/packages/parser/src/types/schema.ts +++ b/packages/parser/src/types/schema.ts @@ -90,31 +90,31 @@ type VpcLatticeEvent = z.infer; type VpcLatticeEventV2 = z.infer; -export { - type ALBEvent, - type ALBMultiValueHeadersEvent, - type APIGatewayProxyEvent, - type APIGatewayProxyEventV2, - type CloudFormationCustomResourceCreateEvent, - type CloudFormationCustomResourceDeleteEvent, - type CloudFormationCustomResourceUpdateEvent, - type CloudWatchLogsEvent, - type DynamoDBStreamEvent, - type EventBridgeEvent, - type KafkaSelfManagedEvent, - type KafkaMskEvent, - type KinesisDataStreamEvent, - type KinesisDataStreamRecord, - type KinesisDataStreamRecordPayload, - type KinesisFireHoseEvent, - type KinesisFireHoseSqsEvent, - type LambdaFunctionUrlEvent, - type S3Event, - type S3EventNotificationEventBridge, - type S3SqsEventNotification, - type SesEvent, - type SnsEvent, - type SqsEvent, - type VpcLatticeEvent, - type VpcLatticeEventV2, +export type { + ALBEvent, + ALBMultiValueHeadersEvent, + APIGatewayProxyEvent, + APIGatewayProxyEventV2, + CloudFormationCustomResourceCreateEvent, + CloudFormationCustomResourceDeleteEvent, + CloudFormationCustomResourceUpdateEvent, + CloudWatchLogsEvent, + DynamoDBStreamEvent, + EventBridgeEvent, + KafkaSelfManagedEvent, + KafkaMskEvent, + KinesisDataStreamEvent, + KinesisDataStreamRecord, + KinesisDataStreamRecordPayload, + KinesisFireHoseEvent, + KinesisFireHoseSqsEvent, + LambdaFunctionUrlEvent, + S3Event, + S3EventNotificationEventBridge, + S3SqsEventNotification, + SesEvent, + SnsEvent, + SqsEvent, + VpcLatticeEvent, + VpcLatticeEventV2, }; diff --git a/packages/parser/tests/unit/envelope.test.ts b/packages/parser/tests/unit/envelope.test.ts new file mode 100644 index 0000000000..1f92ed6e64 --- /dev/null +++ b/packages/parser/tests/unit/envelope.test.ts @@ -0,0 +1,83 @@ +import { z, ZodError } from 'zod'; +import { Envelope } from '../../src/envelopes/envelope.js'; + +describe('envelope: ', () => { + describe('parseSafe', () => { + it('returns success response when input is object', () => { + const result = Envelope.safeParse( + '{"name": "John"}', + z.object({ name: z.string() }) + ); + expect(result).toEqual({ + success: true, + data: { name: 'John' }, + }); + }); + it('returns success response when input is string', () => { + const result = Envelope.safeParse( + { name: 'John' }, + z.object({ name: z.string() }) + ); + expect(result).toEqual({ + success: true, + data: { name: 'John' }, + }); + }); + it('returns error when input does not match schema', () => { + const result = Envelope.safeParse( + { name: 123 }, + z.object({ name: z.string() }) + ); + expect(result).toEqual({ + success: false, + error: expect.any(ZodError), + originalEvent: { name: 123 }, + }); + }); + + it('returns error when input is invalid JSON string', () => { + let err: unknown; + try { + JSON.parse('{name: "John"}'); + } catch (e) { + err = e; + } + const result = Envelope.safeParse( + '{name: "John"}', + z.object({ name: z.string() }) + ); + expect(result).toEqual({ + success: false, + error: err, + originalEvent: '{name: "John"}', + }); + }); + }); + + describe('parse', () => { + it('returns parsed data when input is object', () => { + const result = Envelope.parse( + { name: 'John' }, + z.object({ name: z.string() }) + ); + expect(result).toEqual({ name: 'John' }); + }); + it('returns parsed data when input is string', () => { + const result = Envelope.parse( + '{"name": "John"}', + z.object({ name: z.string() }) + ); + expect(result).toEqual({ name: 'John' }); + }); + it('throw custom error if input is not string or object', () => { + expect(() => Envelope.parse(123, z.object({ name: z.string() }))).toThrow( + 'Invalid data type for envelope. Expected string or object, got number' + ); + }); + it('throws error when input does not match schema', () => { + expect(() => + Envelope.parse({ name: 123 }, z.object({ name: z.string() })) + ).toThrow(); + }); + }); +}); diff --git a/packages/parser/tests/unit/envelopes/apigwt.test.ts b/packages/parser/tests/unit/envelopes/apigwt.test.ts index 03417fcc67..c9df0c03c7 100644 --- a/packages/parser/tests/unit/envelopes/apigwt.test.ts +++ b/packages/parser/tests/unit/envelopes/apigwt.test.ts @@ -7,23 +7,82 @@ import { generateMock } from '@anatine/zod-mock'; import { TestEvents, TestSchema } from '../schema/utils.js'; import { APIGatewayProxyEvent } from '../../../src/types/'; -import { apiGatewayEnvelope } from '../../../src/envelopes/'; +import { ApiGatewayEnvelope } from '../../../src/envelopes/index.js'; +import { ZodError } from 'zod'; describe('ApigwEnvelope ', () => { - it('should parse custom schema in envelope', () => { - const testCustomSchemaObject = generateMock(TestSchema); - const testEvent = TestEvents.apiGatewayProxyEvent as APIGatewayProxyEvent; + describe('parse', () => { + it('should parse custom schema in envelope', () => { + const testCustomSchemaObject = generateMock(TestSchema); + const testEvent = TestEvents.apiGatewayProxyEvent as APIGatewayProxyEvent; - testEvent.body = JSON.stringify(testCustomSchemaObject); + testEvent.body = JSON.stringify(testCustomSchemaObject); - const resp = apiGatewayEnvelope(testEvent, TestSchema); - expect(resp).toEqual(testCustomSchemaObject); + const resp = ApiGatewayEnvelope.parse(testEvent, TestSchema); + expect(resp).toEqual(testCustomSchemaObject); + }); + + it('should throw no body provided', () => { + const testEvent = TestEvents.apiGatewayProxyEvent as APIGatewayProxyEvent; + testEvent.body = undefined; + + expect(() => ApiGatewayEnvelope.parse(testEvent, TestSchema)).toThrow(); + }); + it('should throw invalid event provided', () => { + const testEvent = TestEvents.apiGatewayProxyEvent as APIGatewayProxyEvent; + testEvent.body = 'invalid'; + + expect(() => ApiGatewayEnvelope.parse(testEvent, TestSchema)).toThrow(); + }); }); - it('should throw no body provided', () => { - const testEvent = TestEvents.apiGatewayProxyEvent as APIGatewayProxyEvent; - testEvent.body = undefined; + describe('safeParse', () => { + it('should parse custom schema in envelope', () => { + const testCustomSchemaObject = generateMock(TestSchema); + const testEvent = TestEvents.apiGatewayProxyEvent as APIGatewayProxyEvent; + + testEvent.body = JSON.stringify(testCustomSchemaObject); + + const resp = ApiGatewayEnvelope.safeParse(testEvent, TestSchema); + expect(resp).toEqual({ + success: true, + data: testCustomSchemaObject, + }); + }); + + it('should return success false with original body if no body provided', () => { + const testEvent = TestEvents.apiGatewayProxyEvent as APIGatewayProxyEvent; + testEvent.body = undefined; + + const resp = ApiGatewayEnvelope.safeParse(testEvent, TestSchema); + expect(resp).toEqual({ + success: false, + error: expect.any(Error), + originalEvent: testEvent, + }); + }); + + it('should return success false with original body if invalid body provided', () => { + const testEvent = TestEvents.apiGatewayProxyEvent as APIGatewayProxyEvent; + testEvent.body = 'invalid'; - expect(() => apiGatewayEnvelope(testEvent, TestSchema)).toThrow(); + const resp = ApiGatewayEnvelope.safeParse(testEvent, TestSchema); + expect(resp).toEqual({ + success: false, + error: expect.any(SyntaxError), + originalEvent: testEvent, + }); + }); + it('should return success false if event is invalid', () => { + const resp = ApiGatewayEnvelope.safeParse( + 'invalid' as unknown, + TestSchema + ); + expect(resp).toEqual({ + success: false, + error: expect.any(ZodError), + originalEvent: 'invalid', + }); + }); }); }); diff --git a/packages/parser/tests/unit/envelopes/apigwv2.test.ts b/packages/parser/tests/unit/envelopes/apigwv2.test.ts index 0d2d225bf1..dd5a82609b 100644 --- a/packages/parser/tests/unit/envelopes/apigwv2.test.ts +++ b/packages/parser/tests/unit/envelopes/apigwv2.test.ts @@ -7,24 +7,88 @@ import { TestEvents, TestSchema } from '../schema/utils.js'; import { generateMock } from '@anatine/zod-mock'; import { APIGatewayProxyEventV2 } from 'aws-lambda'; -import { apiGatewayV2Envelope } from '../../../src/envelopes/'; +import { ApiGatewayV2Envelope } from '../../../src/envelopes/index.js'; describe('ApiGwV2Envelope ', () => { - it('should parse custom schema in envelope', () => { - const testEvent = - TestEvents.apiGatewayProxyV2Event as APIGatewayProxyEventV2; - const data = generateMock(TestSchema); + describe('parse', () => { + it('should parse custom schema in envelope', () => { + const testEvent = + TestEvents.apiGatewayProxyV2Event as APIGatewayProxyEventV2; + const data = generateMock(TestSchema); - testEvent.body = JSON.stringify(data); + testEvent.body = JSON.stringify(data); - expect(apiGatewayV2Envelope(testEvent, TestSchema)).toEqual(data); + expect(ApiGatewayV2Envelope.parse(testEvent, TestSchema)).toEqual(data); + }); + + it('should throw when no body provided', () => { + const testEvent = + TestEvents.apiGatewayProxyV2Event as APIGatewayProxyEventV2; + testEvent.body = undefined; + + expect(() => ApiGatewayV2Envelope.parse(testEvent, TestSchema)).toThrow(); + }); + + it('should throw when invalid body provided', () => { + const testEvent = + TestEvents.apiGatewayProxyV2Event as APIGatewayProxyEventV2; + testEvent.body = 'invalid'; + + expect(() => ApiGatewayV2Envelope.parse(testEvent, TestSchema)).toThrow(); + }); + it('should throw when invalid event provided', () => { + expect(() => + ApiGatewayV2Envelope.parse({ foo: 'bar' }, TestSchema) + ).toThrow(); + }); }); - it('should throw when no body provided', () => { - const testEvent = - TestEvents.apiGatewayProxyV2Event as APIGatewayProxyEventV2; - testEvent.body = undefined; + describe('safeParse', () => { + it('should parse custom schema in envelope', () => { + const testEvent = + TestEvents.apiGatewayProxyV2Event as APIGatewayProxyEventV2; + const data = generateMock(TestSchema); + + testEvent.body = JSON.stringify(data); + + expect(ApiGatewayV2Envelope.safeParse(testEvent, TestSchema)).toEqual({ + success: true, + data, + }); + }); + + it('should return success false with original body if no body provided', () => { + const testEvent = + TestEvents.apiGatewayProxyV2Event as APIGatewayProxyEventV2; + testEvent.body = undefined; + + expect(ApiGatewayV2Envelope.safeParse(testEvent, TestSchema)).toEqual({ + success: false, + error: expect.any(Error), + originalEvent: testEvent, + }); + }); + + it('should return success false with original body if invalid body provided', () => { + const testEvent = + TestEvents.apiGatewayProxyV2Event as APIGatewayProxyEventV2; + testEvent.body = 'invalid'; + + expect(ApiGatewayV2Envelope.safeParse(testEvent, TestSchema)).toEqual({ + success: false, + error: expect.any(Error), + originalEvent: testEvent, + }); + }); - expect(() => apiGatewayV2Envelope(testEvent, TestSchema)).toThrow(); + it('should return success false with original event if invalid event provided', () => { + expect( + ApiGatewayV2Envelope.safeParse({ foo: 'bar' }, TestSchema) + ).toEqual({ + success: false, + error: expect.any(Error), + originalEvent: { foo: 'bar' }, + }); + }); }); }); diff --git a/packages/parser/tests/unit/envelopes/cloudwatch.test.ts b/packages/parser/tests/unit/envelopes/cloudwatch.test.ts index e47d85f26d..adfe6e52f9 100644 --- a/packages/parser/tests/unit/envelopes/cloudwatch.test.ts +++ b/packages/parser/tests/unit/envelopes/cloudwatch.test.ts @@ -11,53 +11,121 @@ import { CloudWatchLogsDecodeSchema, } from '../../../src/schemas/'; import { TestSchema } from '../schema/utils.js'; -import { cloudWatchEnvelope } from '../../../src/envelopes/'; +import { CloudWatchEnvelope } from '../../../src/envelopes/index.js'; +import { ZodError } from 'zod'; describe('CloudWatch', () => { - it('should parse custom schema in envelope', () => { - const testEvent = { - awslogs: { - data: '', - }, - }; - - const data = generateMock(TestSchema); - const eventMock = generateMock(CloudWatchLogEventSchema, { - stringMap: { - message: () => JSON.stringify(data), - }, + describe('parse', () => { + it('should parse custom schema in envelope', () => { + const testEvent = { + awslogs: { + data: '', + }, + }; + + const data = generateMock(TestSchema); + const eventMock = generateMock(CloudWatchLogEventSchema, { + stringMap: { + message: () => JSON.stringify(data), + }, + }); + + const logMock = generateMock(CloudWatchLogsDecodeSchema); + logMock.logEvents = [eventMock]; + + testEvent.awslogs.data = gzipSync( + Buffer.from(JSON.stringify(logMock), 'utf8') + ).toString('base64'); + + expect(CloudWatchEnvelope.parse(testEvent, TestSchema)).toEqual([data]); }); - const logMock = generateMock(CloudWatchLogsDecodeSchema); - logMock.logEvents = [eventMock]; + it('should throw when schema does not match', () => { + const testEvent = { + awslogs: { + data: '', + }, + }; - testEvent.awslogs.data = gzipSync( - Buffer.from(JSON.stringify(logMock), 'utf8') - ).toString('base64'); + const eventMock = generateMock(CloudWatchLogEventSchema, { + stringMap: { + message: () => JSON.stringify({ foo: 'bar' }), + }, + }); - expect(cloudWatchEnvelope(testEvent, TestSchema)).toEqual([data]); + const logMock = generateMock(CloudWatchLogsDecodeSchema); + logMock.logEvents = [eventMock]; + + testEvent.awslogs.data = gzipSync( + Buffer.from(JSON.stringify(logMock), 'utf8') + ).toString('base64'); + + expect(() => CloudWatchEnvelope.parse(testEvent, TestSchema)).toThrow(); + }); }); - it('should throw when schema does not match', () => { - const testEvent = { - awslogs: { - data: '', - }, - }; - - const eventMock = generateMock(CloudWatchLogEventSchema, { - stringMap: { - message: () => JSON.stringify({ foo: 'bar' }), - }, + describe('safeParse', () => { + it('should parse custom schema in envelope', () => { + const testEvent = { + awslogs: { + data: '', + }, + }; + + const data = generateMock(TestSchema); + const eventMock = generateMock(CloudWatchLogEventSchema, { + stringMap: { + message: () => JSON.stringify(data), + }, + }); + + const logMock = generateMock(CloudWatchLogsDecodeSchema); + logMock.logEvents = [eventMock]; + + testEvent.awslogs.data = gzipSync( + Buffer.from(JSON.stringify(logMock), 'utf8') + ).toString('base64'); + + const actual = CloudWatchEnvelope.safeParse(testEvent, TestSchema); + expect(actual).toEqual({ + success: true, + data: [data], + }); }); - const logMock = generateMock(CloudWatchLogsDecodeSchema); - logMock.logEvents = [eventMock]; + it('should return success false when schema does not match', () => { + const testEvent = { + awslogs: { + data: '', + }, + }; + + const eventMock = generateMock(CloudWatchLogEventSchema, { + stringMap: { + message: () => JSON.stringify({ foo: 'bar' }), + }, + }); - testEvent.awslogs.data = gzipSync( - Buffer.from(JSON.stringify(logMock), 'utf8') - ).toString('base64'); + const logMock = generateMock(CloudWatchLogsDecodeSchema); + logMock.logEvents = [eventMock]; - expect(() => cloudWatchEnvelope(testEvent, TestSchema)).toThrow(); + testEvent.awslogs.data = gzipSync( + Buffer.from(JSON.stringify(logMock), 'utf8') + ).toString('base64'); + + expect(CloudWatchEnvelope.safeParse(testEvent, TestSchema)).toEqual({ + success: false, + error: expect.any(Error), + originalEvent: testEvent, + }); + }); + + it('should return success false when envelope does not match', () => { + expect(CloudWatchEnvelope.safeParse({ foo: 'bar' }, TestSchema)).toEqual({ + success: false, + error: expect.any(ZodError), + originalEvent: { foo: 'bar' }, + }); + }); }); }); diff --git a/packages/parser/tests/unit/envelopes/dynamodb.test.ts b/packages/parser/tests/unit/envelopes/dynamodb.test.ts index 0c9c940daf..4d2c9fb657 100644 --- a/packages/parser/tests/unit/envelopes/dynamodb.test.ts +++ b/packages/parser/tests/unit/envelopes/dynamodb.test.ts @@ -7,37 +7,122 @@ import { generateMock } from '@anatine/zod-mock'; import { TestEvents } from '../schema/utils.js'; import { DynamoDBStreamEvent } from 'aws-lambda'; -import { z } from 'zod'; -import { dynamoDDStreamEnvelope } from '../../../src/envelopes/'; +import { z, ZodError } from 'zod'; +import { DynamoDBStreamEnvelope } from '../../../src/envelopes/index.js'; describe('DynamoDB', () => { const schema = z.object({ Message: z.record(z.literal('S'), z.string()), Id: z.record(z.literal('N'), z.number().min(0).max(100)), }); + const mockOldImage = generateMock(schema); + const mockNewImage = generateMock(schema); + const dynamodbEvent = TestEvents.dynamoStreamEvent as DynamoDBStreamEvent; + (dynamodbEvent.Records[0].dynamodb!.NewImage as typeof mockNewImage) = + mockNewImage; + (dynamodbEvent.Records[1].dynamodb!.NewImage as typeof mockNewImage) = + mockNewImage; + (dynamodbEvent.Records[0].dynamodb!.OldImage as typeof mockOldImage) = + mockOldImage; + (dynamodbEvent.Records[1].dynamodb!.OldImage as typeof mockOldImage) = + mockOldImage; + describe('parse', () => { + it('parse should parse dynamodb envelope', () => { + const parsed = DynamoDBStreamEnvelope.parse(dynamodbEvent, schema); + expect(parsed[0]).toEqual({ + OldImage: mockOldImage, + NewImage: mockNewImage, + }); + expect(parsed[1]).toEqual({ + OldImage: mockOldImage, + NewImage: mockNewImage, + }); + }); + it('parse should throw error if envelope invalid', () => { + expect(() => + DynamoDBStreamEnvelope.parse({ foo: 'bar' }, schema) + ).toThrow(); + }); + it('parse should throw error if new or old image is invalid', () => { + const ddbEvent = TestEvents.dynamoStreamEvent as DynamoDBStreamEvent; + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + ddbEvent.Records[0].dynamodb!.NewImage.Id = 'foo'; + expect(() => DynamoDBStreamEnvelope.parse(ddbEvent, schema)).toThrow(); + }); + }); + + describe('safeParse', () => { + it('safeParse should parse dynamodb envelope', () => { + const parsed = DynamoDBStreamEnvelope.safeParse(dynamodbEvent, schema); + expect(parsed.success).toBe(true); + expect(parsed).toEqual({ + success: true, + data: [ + { + OldImage: mockOldImage, + NewImage: mockNewImage, + }, + { + OldImage: mockOldImage, + NewImage: mockNewImage, + }, + ], + }); + }); + it('safeParse should return error if NewImage is invalid', () => { + const invalidDDBEvent = + TestEvents.dynamoStreamEvent as DynamoDBStreamEvent; - it('should parse dynamodb envelope', () => { - const mockOldImage = generateMock(schema); - const mockNewImage = generateMock(schema); - const dynamodbEvent = TestEvents.dynamoStreamEvent as DynamoDBStreamEvent; - - (dynamodbEvent.Records[0].dynamodb!.NewImage as typeof mockNewImage) = - mockNewImage; - (dynamodbEvent.Records[1].dynamodb!.NewImage as typeof mockNewImage) = - mockNewImage; - (dynamodbEvent.Records[0].dynamodb!.OldImage as typeof mockOldImage) = - mockOldImage; - (dynamodbEvent.Records[1].dynamodb!.OldImage as typeof mockOldImage) = - mockOldImage; - - const parsed = dynamoDDStreamEnvelope(dynamodbEvent, schema); - expect(parsed[0]).toEqual({ - OldImage: mockOldImage, - NewImage: mockNewImage, + (invalidDDBEvent.Records[0].dynamodb!.NewImage as typeof mockNewImage) = { + Id: { N: 101 }, + Message: { S: 'foo' }, + }; + (invalidDDBEvent.Records[1].dynamodb!.NewImage as typeof mockNewImage) = + mockNewImage; + (invalidDDBEvent.Records[0].dynamodb!.OldImage as typeof mockOldImage) = + mockOldImage; + (invalidDDBEvent.Records[1].dynamodb!.OldImage as typeof mockOldImage) = + mockOldImage; + + const parsed = DynamoDBStreamEnvelope.safeParse(invalidDDBEvent, schema); + expect(parsed).toEqual({ + success: false, + error: expect.any(ZodError), + originalEvent: invalidDDBEvent, + }); + }); + + it('safeParse should return error if OldImage is invalid', () => { + const invalidDDBEvent = + TestEvents.dynamoStreamEvent as DynamoDBStreamEvent; + + (invalidDDBEvent.Records[0].dynamodb!.OldImage as typeof mockNewImage) = { + Id: { N: 101 }, + Message: { S: 'foo' }, + }; + (invalidDDBEvent.Records[1].dynamodb!.NewImage as typeof mockNewImage) = + mockNewImage; + (invalidDDBEvent.Records[0].dynamodb!.OldImage as typeof mockOldImage) = + mockOldImage; + (invalidDDBEvent.Records[0].dynamodb!.NewImage as typeof mockNewImage) = + mockNewImage; + + const parsed = DynamoDBStreamEnvelope.safeParse(invalidDDBEvent, schema); + expect(parsed).toEqual({ + success: false, + error: expect.any(ZodError), + originalEvent: invalidDDBEvent, + }); }); - expect(parsed[1]).toEqual({ - OldImage: mockOldImage, - NewImage: mockNewImage, + + it('safeParse should return error if envelope is invalid', () => { + const parsed = DynamoDBStreamEnvelope.safeParse({ foo: 'bar' }, schema); + expect(parsed).toEqual({ + success: false, + error: expect.any(ZodError), + originalEvent: { foo: 'bar' }, + }); }); }); }); diff --git a/packages/parser/tests/unit/envelopes/eventbridge.test.ts b/packages/parser/tests/unit/envelopes/eventbridge.test.ts index 5baaf12f9f..746d95b160 100644 --- a/packages/parser/tests/unit/envelopes/eventbridge.test.ts +++ b/packages/parser/tests/unit/envelopes/eventbridge.test.ts @@ -7,45 +7,116 @@ import { TestEvents, TestSchema } from '../schema/utils.js'; import { generateMock } from '@anatine/zod-mock'; import { EventBridgeEvent } from 'aws-lambda'; -import { eventBridgeEnvelope } from '../../../src/envelopes/'; +import { ZodError } from 'zod'; +import { EventBridgeEnvelope } from '../../../src/envelopes/index.js'; describe('EventBridgeEnvelope ', () => { - it('should parse eventbridge event', () => { - const eventBridgeEvent = TestEvents.eventBridgeEvent as EventBridgeEvent< - string, - object - >; + describe('parse', () => { + it('should parse eventbridge event', () => { + const eventBridgeEvent = TestEvents.eventBridgeEvent as EventBridgeEvent< + string, + object + >; - const data = generateMock(TestSchema); + const data = generateMock(TestSchema); - eventBridgeEvent.detail = data; + eventBridgeEvent.detail = data; - expect(eventBridgeEnvelope(eventBridgeEvent, TestSchema)).toEqual(data); - }); + expect(EventBridgeEnvelope.parse(eventBridgeEvent, TestSchema)).toEqual( + data + ); + }); + + it('should throw error if detail type does not match schema', () => { + const eventBridgeEvent = TestEvents.eventBridgeEvent as EventBridgeEvent< + string, + object + >; + + eventBridgeEvent.detail = { + foo: 'bar', + }; - it('should throw error if detail type does not match schema', () => { - const eventBridgeEvent = TestEvents.eventBridgeEvent as EventBridgeEvent< - string, - object - >; + expect(() => + EventBridgeEnvelope.parse(eventBridgeEvent, TestSchema) + ).toThrowError(); + }); - eventBridgeEvent.detail = { - foo: 'bar', - }; + it('should throw when invalid data type provided', () => { + const eventBridgeEvent = TestEvents.eventBridgeEvent as EventBridgeEvent< + string, + object + >; - expect(() => - eventBridgeEnvelope(eventBridgeEvent, TestSchema) - ).toThrowError(); + eventBridgeEvent.detail = 1 as unknown as object; + + expect(() => + EventBridgeEnvelope.parse(eventBridgeEvent, TestSchema) + ).toThrow(); + }); }); - it('should throw when invalid data type provided', () => { - const eventBridgeEvent = TestEvents.eventBridgeEvent as EventBridgeEvent< - string, - object - >; + describe('safeParse', () => { + it('should safe parse eventbridge event', () => { + const eventBridgeEvent = TestEvents.eventBridgeEvent as EventBridgeEvent< + string, + object + >; + + const data = generateMock(TestSchema); + + eventBridgeEvent.detail = data; + + expect( + EventBridgeEnvelope.safeParse(eventBridgeEvent, TestSchema) + ).toEqual({ + success: true, + data: data, + }); + }); + + it('should safe parse eventbridge event and return original event if invalid', () => { + const eventBridgeEvent = TestEvents.eventBridgeEvent as EventBridgeEvent< + string, + object + >; + + eventBridgeEvent.detail = { + foo: 'bar', + }; + + expect( + EventBridgeEnvelope.safeParse(eventBridgeEvent, TestSchema) + ).toEqual({ + success: false, + error: expect.any(ZodError), + originalEvent: eventBridgeEvent, + }); + }); + + it('should safe parse eventbridge event and return original event if invalid data type', () => { + const eventBridgeEvent = TestEvents.eventBridgeEvent as EventBridgeEvent< + string, + object + >; + + eventBridgeEvent.detail = 1 as unknown as object; - eventBridgeEvent.detail = 1 as unknown as object; + expect( + EventBridgeEnvelope.safeParse(eventBridgeEvent, TestSchema) + ).toEqual({ + success: false, + error: expect.any(Error), + originalEvent: eventBridgeEvent, + }); + }); - expect(() => eventBridgeEnvelope(eventBridgeEvent, TestSchema)).toThrow(); + it('should return original event and error envelope is invalid', () => { + expect(EventBridgeEnvelope.safeParse(1, TestSchema)).toEqual({ + success: false, + error: expect.any(ZodError), + originalEvent: 1, + }); + }); }); }); diff --git a/packages/parser/tests/unit/envelopes/kafka.test.ts b/packages/parser/tests/unit/envelopes/kafka.test.ts index 37657bf93a..5c55013244 100644 --- a/packages/parser/tests/unit/envelopes/kafka.test.ts +++ b/packages/parser/tests/unit/envelopes/kafka.test.ts @@ -7,33 +7,90 @@ import { generateMock } from '@anatine/zod-mock'; import { TestEvents, TestSchema } from '../schema/utils.js'; import { MSKEvent, SelfManagedKafkaEvent } from 'aws-lambda'; -import { kafkaEnvelope } from '../../../src/envelopes/'; +import { KafkaEnvelope } from '../../../src/envelopes/index.js'; describe('Kafka', () => { - it('should parse MSK kafka envelope', () => { - const mock = generateMock(TestSchema); + describe('parse', () => { + it('should parse MSK kafka envelope', () => { + const mock = generateMock(TestSchema); - const kafkaEvent = TestEvents.kafkaEventMsk as MSKEvent; - kafkaEvent.records['mytopic-0'][0].value = Buffer.from( - JSON.stringify(mock) - ).toString('base64'); + const kafkaEvent = TestEvents.kafkaEventMsk as MSKEvent; + kafkaEvent.records['mytopic-0'][0].value = Buffer.from( + JSON.stringify(mock) + ).toString('base64'); - const result = kafkaEnvelope(kafkaEvent, TestSchema); + const result = KafkaEnvelope.parse(kafkaEvent, TestSchema); - expect(result).toEqual([[mock]]); - }); + expect(result).toEqual([[mock]]); + }); + + it('should parse Self Managed kafka envelope', () => { + const mock = generateMock(TestSchema); + + const kafkaEvent = + TestEvents.kafkaEventSelfManaged as SelfManagedKafkaEvent; + kafkaEvent.records['mytopic-0'][0].value = Buffer.from( + JSON.stringify(mock) + ).toString('base64'); + + const result = KafkaEnvelope.parse(kafkaEvent, TestSchema); + + expect(result).toEqual([[mock]]); + }); + + describe('safeParse', () => { + it('should parse MSK kafka envelope', () => { + const mock = generateMock(TestSchema); + + const kafkaEvent = TestEvents.kafkaEventMsk as MSKEvent; + kafkaEvent.records['mytopic-0'][0].value = Buffer.from( + JSON.stringify(mock) + ).toString('base64'); + + const result = KafkaEnvelope.safeParse(kafkaEvent, TestSchema); + + expect(result).toEqual({ + success: true, + data: [mock], + }); + }); + + it('should parse Self Managed kafka envelope', () => { + const mock = generateMock(TestSchema); + + const kafkaEvent = + TestEvents.kafkaEventSelfManaged as SelfManagedKafkaEvent; + kafkaEvent.records['mytopic-0'][0].value = Buffer.from( + JSON.stringify(mock) + ).toString('base64'); + + const result = KafkaEnvelope.safeParse(kafkaEvent, TestSchema); - it('should parse Self Managed kafka envelope', () => { - const mock = generateMock(TestSchema); + expect(result).toEqual({ + success: true, + data: [mock], + }); + }); - const kafkaEvent = - TestEvents.kafkaEventSelfManaged as SelfManagedKafkaEvent; - kafkaEvent.records['mytopic-0'][0].value = Buffer.from( - JSON.stringify(mock) - ).toString('base64'); + it('should return original event on failure', () => { + const kafkaEvent = TestEvents.kafkaEventMsk as MSKEvent; + kafkaEvent.records['mytopic-0'][0].value = 'not a valid json'; - const result = kafkaEnvelope(kafkaEvent, TestSchema); + const result = KafkaEnvelope.safeParse(kafkaEvent, TestSchema); - expect(result).toEqual([[mock]]); + expect(result).toEqual({ + success: false, + error: expect.any(Error), + originalEvent: kafkaEvent, + }); + }); + it('should return original event and error if envelope is invalid', () => { + expect(KafkaEnvelope.safeParse({ foo: 'bar' }, TestSchema)).toEqual({ + success: false, + error: expect.any(Error), + originalEvent: { foo: 'bar' }, + }); + }); + }); }); }); diff --git a/packages/parser/tests/unit/envelopes/kinesis-firehose.test.ts b/packages/parser/tests/unit/envelopes/kinesis-firehose.test.ts index 65c7be5fd5..50951d7c0c 100644 --- a/packages/parser/tests/unit/envelopes/kinesis-firehose.test.ts +++ b/packages/parser/tests/unit/envelopes/kinesis-firehose.test.ts @@ -8,48 +8,157 @@ import { TestEvents, TestSchema } from '../schema/utils.js'; import { generateMock } from '@anatine/zod-mock'; import { KinesisFirehoseSchema } from '../../../src/schemas/'; import { z } from 'zod'; -import { kinesisFirehoseEnvelope } from '../../../src/envelopes/'; +import { KinesisFirehoseEnvelope } from '../../../src/envelopes/index.js'; describe('Kinesis Firehose Envelope', () => { - it('should parse records for PutEvent', () => { - const mock = generateMock(TestSchema); - const testEvent = TestEvents.kinesisFirehosePutEvent as z.infer< - typeof KinesisFirehoseSchema - >; + describe('parse', () => { + it('should parse records for PutEvent', () => { + const mock = generateMock(TestSchema); + const testEvent = TestEvents.kinesisFirehosePutEvent as z.infer< + typeof KinesisFirehoseSchema + >; - testEvent.records.map((record) => { - record.data = Buffer.from(JSON.stringify(mock)).toString('base64'); + testEvent.records.map((record) => { + record.data = Buffer.from(JSON.stringify(mock)).toString('base64'); + }); + + const resp = KinesisFirehoseEnvelope.parse(testEvent, TestSchema); + expect(resp).toEqual([mock, mock]); }); - const resp = kinesisFirehoseEnvelope(testEvent, TestSchema); - expect(resp).toEqual([mock, mock]); - }); + it('should parse a single record for SQS event', () => { + const mock = generateMock(TestSchema); + const testEvent = TestEvents.kinesisFirehoseSQSEvent as z.infer< + typeof KinesisFirehoseSchema + >; + + testEvent.records.map((record) => { + record.data = Buffer.from(JSON.stringify(mock)).toString('base64'); + }); + + const resp = KinesisFirehoseEnvelope.parse(testEvent, TestSchema); + expect(resp).toEqual([mock]); + }); + + it('should parse records for kinesis event', () => { + const mock = generateMock(TestSchema); + const testEvent = TestEvents.kinesisFirehoseKinesisEvent as z.infer< + typeof KinesisFirehoseSchema + >; - it('should parse a single record for SQS event', () => { - const mock = generateMock(TestSchema); - const testEvent = TestEvents.kinesisFirehoseSQSEvent as z.infer< - typeof KinesisFirehoseSchema - >; + testEvent.records.map((record) => { + record.data = Buffer.from(JSON.stringify(mock)).toString('base64'); + }); - testEvent.records.map((record) => { - record.data = Buffer.from(JSON.stringify(mock)).toString('base64'); + const resp = KinesisFirehoseEnvelope.parse(testEvent, TestSchema); + expect(resp).toEqual([mock, mock]); }); + it('should throw if record is not base64 encoded', () => { + const testEvent = TestEvents.kinesisFirehosePutEvent as z.infer< + typeof KinesisFirehoseSchema + >; - const resp = kinesisFirehoseEnvelope(testEvent, TestSchema); - expect(resp).toEqual([mock]); + testEvent.records.map((record) => { + record.data = 'not base64 encoded'; + }); + + expect(() => { + KinesisFirehoseEnvelope.parse(testEvent, TestSchema); + }).toThrow(); + }); + it('should throw if envelope is invalid', () => { + expect(() => { + KinesisFirehoseEnvelope.parse({ foo: 'bar' }, TestSchema); + }).toThrow(); + }); + it('should throw when schema does not match record', () => { + const testEvent = TestEvents.kinesisFirehosePutEvent as z.infer< + typeof KinesisFirehoseSchema + >; + + testEvent.records.map((record) => { + record.data = Buffer.from('not a valid json').toString('base64'); + }); + + expect(() => { + KinesisFirehoseEnvelope.parse(testEvent, TestSchema); + }).toThrow(); + }); }); + describe('safeParse', () => { + it('should parse records for PutEvent', () => { + const mock = generateMock(TestSchema); + const testEvent = TestEvents.kinesisFirehosePutEvent as z.infer< + typeof KinesisFirehoseSchema + >; - it('should parse records for kinesis event', () => { - const mock = generateMock(TestSchema); - const testEvent = TestEvents.kinesisFirehoseKinesisEvent as z.infer< - typeof KinesisFirehoseSchema - >; + testEvent.records.map((record) => { + record.data = Buffer.from(JSON.stringify(mock)).toString('base64'); + }); - testEvent.records.map((record) => { - record.data = Buffer.from(JSON.stringify(mock)).toString('base64'); + const resp = KinesisFirehoseEnvelope.safeParse(testEvent, TestSchema); + expect(resp).toEqual({ success: true, data: [mock, mock] }); }); - const resp = kinesisFirehoseEnvelope(testEvent, TestSchema); - expect(resp).toEqual([mock, mock]); + it('should parse a single record for SQS event', () => { + const mock = generateMock(TestSchema); + const testEvent = TestEvents.kinesisFirehoseSQSEvent as z.infer< + typeof KinesisFirehoseSchema + >; + + testEvent.records.map((record) => { + record.data = Buffer.from(JSON.stringify(mock)).toString('base64'); + }); + + const resp = KinesisFirehoseEnvelope.safeParse(testEvent, TestSchema); + expect(resp).toEqual({ success: true, data: [mock] }); + }); + + it('should parse records for kinesis event', () => { + const mock = generateMock(TestSchema); + const testEvent = TestEvents.kinesisFirehoseKinesisEvent as z.infer< + typeof KinesisFirehoseSchema + >; + + testEvent.records.map((record) => { + record.data = Buffer.from(JSON.stringify(mock)).toString('base64'); + }); + + const resp = KinesisFirehoseEnvelope.safeParse(testEvent, TestSchema); + expect(resp).toEqual({ success: true, data: [mock, mock] }); + }); + it('should return original event if envelope is invalid', () => { + expect( + KinesisFirehoseEnvelope.safeParse({ foo: 'bar' }, TestSchema) + ).toEqual({ + success: false, + error: expect.any(Error), + originalEvent: { foo: 'bar' }, + }); + }); + it('should return original event if record is not base64 encoded', () => { + const testEvent = TestEvents.kinesisFirehosePutEvent as z.infer< + typeof KinesisFirehoseSchema + >; + + testEvent.records.map((record) => { + record.data = 'not base64 encoded'; + }); + + expect(KinesisFirehoseEnvelope.safeParse(testEvent, TestSchema)).toEqual({ + success: false, + error: expect.any(Error), + originalEvent: testEvent, + }); + }); + it('should return original event envelope is invalid', () => { + expect( + KinesisFirehoseEnvelope.safeParse({ foo: 'bar' }, TestSchema) + ).toEqual({ + success: false, + error: expect.any(Error), + originalEvent: { foo: 'bar' }, + }); + }); }); }); diff --git a/packages/parser/tests/unit/envelopes/kinesis.test.ts b/packages/parser/tests/unit/envelopes/kinesis.test.ts index 67dfc464eb..f84149c252 100644 --- a/packages/parser/tests/unit/envelopes/kinesis.test.ts +++ b/packages/parser/tests/unit/envelopes/kinesis.test.ts @@ -7,20 +7,66 @@ import { generateMock } from '@anatine/zod-mock'; import { KinesisStreamEvent } from 'aws-lambda'; import { TestEvents, TestSchema } from '../schema/utils.js'; -import { kinesisEnvelope } from '../../../src/envelopes/'; +import { KinesisEnvelope } from '../../../src/envelopes/index.js'; +import { ZodError } from 'zod'; -describe('Kinesis', () => { - it('should parse Kinesis Stream event', () => { - const mock = generateMock(TestSchema); - const testEvent = TestEvents.kinesisStreamEvent as KinesisStreamEvent; +describe('KinesisEnvelope', () => { + describe('parse', () => { + it('should parse Kinesis Stream event', () => { + const mock = generateMock(TestSchema); + const testEvent = TestEvents.kinesisStreamEvent as KinesisStreamEvent; - testEvent.Records.map((record) => { - record.kinesis.data = Buffer.from(JSON.stringify(mock)).toString( - 'base64' - ); + testEvent.Records.map((record) => { + record.kinesis.data = Buffer.from(JSON.stringify(mock)).toString( + 'base64' + ); + }); + + const resp = KinesisEnvelope.parse(testEvent, TestSchema); + expect(resp).toEqual([mock, mock]); + }); + it('should throw if envelope is invalid', () => { + expect(() => KinesisEnvelope.parse({ foo: 'bar' }, TestSchema)).toThrow(); + }); + it('should throw if record is invalid', () => { + const testEvent = TestEvents.kinesisStreamEvent as KinesisStreamEvent; + testEvent.Records[0].kinesis.data = 'invalid'; + expect(() => KinesisEnvelope.parse(testEvent, TestSchema)).toThrow(); }); + }); + + describe('safeParse', () => { + it('should parse Kinesis Stream event', () => { + const mock = generateMock(TestSchema); + const testEvent = TestEvents.kinesisStreamEvent as KinesisStreamEvent; - const resp = kinesisEnvelope(testEvent, TestSchema); - expect(resp).toEqual([mock, mock]); + testEvent.Records.map((record) => { + record.kinesis.data = Buffer.from(JSON.stringify(mock)).toString( + 'base64' + ); + }); + + const resp = KinesisEnvelope.safeParse(testEvent, TestSchema); + expect(resp).toEqual({ success: true, data: [mock, mock] }); + }); + it('should return original event if envelope is invalid', () => { + const testEvent = { foo: 'bar' }; + const resp = KinesisEnvelope.safeParse(testEvent, TestSchema); + expect(resp).toEqual({ + success: false, + error: expect.any(ZodError), + originalEvent: testEvent, + }); + }); + it('should return original event if record is invalid', () => { + const testEvent = TestEvents.kinesisStreamEvent as KinesisStreamEvent; + testEvent.Records[0].kinesis.data = 'invalid'; + const resp = KinesisEnvelope.safeParse(testEvent, TestSchema); + expect(resp).toEqual({ + success: false, + error: expect.any(SyntaxError), + originalEvent: testEvent, + }); + }); }); }); diff --git a/packages/parser/tests/unit/envelopes/lambda.test.ts b/packages/parser/tests/unit/envelopes/lambda.test.ts index 1cf6e20cea..56b0551cfd 100644 --- a/packages/parser/tests/unit/envelopes/lambda.test.ts +++ b/packages/parser/tests/unit/envelopes/lambda.test.ts @@ -1,30 +1,92 @@ +import { LambdaFunctionUrlEnvelope } from '../../../src/envelopes/index.js'; +import { TestEvents, TestSchema } from '../schema/utils.js'; +import { generateMock } from '@anatine/zod-mock'; +import { APIGatewayProxyEventV2 } from 'aws-lambda'; + /** * Test built in schema envelopes for Lambda Functions URL * * @group unit/parser/envelopes */ -import { TestEvents, TestSchema } from '../schema/utils.js'; -import { generateMock } from '@anatine/zod-mock'; -import { APIGatewayProxyEventV2 } from 'aws-lambda'; -import { lambdaFunctionUrlEnvelope } from '../../../src/envelopes/'; - describe('Lambda Functions Url ', () => { - it('should parse custom schema in envelope', () => { - const testEvent = - TestEvents.lambdaFunctionUrlEvent as APIGatewayProxyEventV2; - const data = generateMock(TestSchema); + describe('parse', () => { + it('should parse custom schema in envelope', () => { + const testEvent = + TestEvents.lambdaFunctionUrlEvent as APIGatewayProxyEventV2; + const data = generateMock(TestSchema); - testEvent.body = JSON.stringify(data); + testEvent.body = JSON.stringify(data); - expect(lambdaFunctionUrlEnvelope(testEvent, TestSchema)).toEqual(data); + expect(LambdaFunctionUrlEnvelope.parse(testEvent, TestSchema)).toEqual( + data + ); + }); + + it('should throw when no body provided', () => { + const testEvent = + TestEvents.apiGatewayProxyV2Event as APIGatewayProxyEventV2; + testEvent.body = undefined; + + expect(() => + LambdaFunctionUrlEnvelope.parse(testEvent, TestSchema) + ).toThrow(); + }); + + it('should throw when envelope is not valid', () => { + expect(() => + LambdaFunctionUrlEnvelope.parse({ foo: 'bar' }, TestSchema) + ).toThrow(); + }); + + it('should throw when body does not match schema', () => { + const testEvent = + TestEvents.lambdaFunctionUrlEvent as APIGatewayProxyEventV2; + testEvent.body = JSON.stringify({ foo: 'bar' }); + + expect(() => + LambdaFunctionUrlEnvelope.parse(testEvent, TestSchema) + ).toThrow(); + }); }); + describe('safeParse', () => { + it('should parse custom schema in envelope', () => { + const testEvent = + TestEvents.lambdaFunctionUrlEvent as APIGatewayProxyEventV2; + const data = generateMock(TestSchema); + + testEvent.body = JSON.stringify(data); + + expect( + LambdaFunctionUrlEnvelope.safeParse(testEvent, TestSchema) + ).toEqual({ + success: true, + data, + }); + }); + + it('should return original event when envelope is not valid', () => { + expect( + LambdaFunctionUrlEnvelope.safeParse({ foo: 'bar' }, TestSchema) + ).toEqual({ + success: false, + error: expect.any(Error), + originalEvent: { foo: 'bar' }, + }); + }); - it('should throw when no body provided', () => { - const testEvent = - TestEvents.apiGatewayProxyV2Event as APIGatewayProxyEventV2; - testEvent.body = undefined; + it('should return original event when body does not match schema', () => { + const testEvent = + TestEvents.lambdaFunctionUrlEvent as APIGatewayProxyEventV2; + testEvent.body = JSON.stringify({ foo: 'bar' }); - expect(() => lambdaFunctionUrlEnvelope(testEvent, TestSchema)).toThrow(); + expect( + LambdaFunctionUrlEnvelope.safeParse(testEvent, TestSchema) + ).toEqual({ + success: false, + error: expect.any(Error), + originalEvent: testEvent, + }); + }); }); }); diff --git a/packages/parser/tests/unit/envelopes/sns.test.ts b/packages/parser/tests/unit/envelopes/sns.test.ts index 0bf36c94c0..c1e9b67bee 100644 --- a/packages/parser/tests/unit/envelopes/sns.test.ts +++ b/packages/parser/tests/unit/envelopes/sns.test.ts @@ -4,48 +4,160 @@ * @group unit/parser/envelopes */ -import { z } from 'zod'; +import { z, ZodError } from 'zod'; import { generateMock } from '@anatine/zod-mock'; import { SNSEvent, SQSEvent } from 'aws-lambda'; import { TestEvents, TestSchema } from '../schema/utils.js'; -import { snsEnvelope, snsSqsEnvelope } from '../../../src/envelopes/'; +import { SnsEnvelope, SnsSqsEnvelope } from '../../../src/envelopes/index.js'; -describe('SNS Envelope', () => { - it('should parse custom schema in envelope', () => { - const testEvent = TestEvents.snsEvent as SNSEvent; +describe('Sns and SQS Envelope', () => { + describe('SnsSqsEnvelope parse', () => { + it('should parse sqs inside sns envelope', () => { + const snsSqsTestEvent = TestEvents.snsSqsEvent as SQSEvent; - const testRecords = [] as z.infer[]; + const data = generateMock(TestSchema); + const snsEvent = JSON.parse(snsSqsTestEvent.Records[0].body); + snsEvent.Message = JSON.stringify(data); - testEvent.Records.map((record) => { - const value = generateMock(TestSchema); - testRecords.push(value); - record.Sns.Message = JSON.stringify(value); - }); + snsSqsTestEvent.Records[0].body = JSON.stringify(snsEvent); - expect(snsEnvelope(testEvent, TestSchema)).toEqual(testRecords); + expect(SnsSqsEnvelope.parse(snsSqsTestEvent, TestSchema)).toEqual([data]); + }); }); + describe('SnsSqsEnvelope safeParse', () => { + it('should parse sqs inside sns envelope', () => { + const snsSqsTestEvent = TestEvents.snsSqsEvent as SQSEvent; + + const data = generateMock(TestSchema); + const snsEvent = JSON.parse(snsSqsTestEvent.Records[0].body); + snsEvent.Message = JSON.stringify(data); + + snsSqsTestEvent.Records[0].body = JSON.stringify(snsEvent); + + expect(SnsSqsEnvelope.safeParse(snsSqsTestEvent, TestSchema)).toEqual({ + success: true, + data: [data], + }); + }); + it('should return error when envelope is not valid', () => { + expect(SnsSqsEnvelope.safeParse({ foo: 'bar' }, TestSchema)).toEqual({ + success: false, + error: expect.any(Error), + originalEvent: { foo: 'bar' }, + }); + }); + it('should return error if message does not match schema', () => { + const snsSqsTestEvent = TestEvents.snsSqsEvent as SQSEvent; + + const snsEvent = JSON.parse(snsSqsTestEvent.Records[0].body); + snsEvent.Message = JSON.stringify({ + foo: 'bar', + }); + + snsSqsTestEvent.Records[0].body = JSON.stringify(snsEvent); - it('should throw if message does not macht schema', () => { - const testEvent = TestEvents.snsEvent as SNSEvent; + expect(SnsSqsEnvelope.safeParse(snsSqsTestEvent, TestSchema)).toEqual({ + success: false, + error: expect.any(ZodError), + originalEvent: snsSqsTestEvent, + }); + }); + it('should return error if sns message is not valid', () => { + const snsSqsTestEvent = TestEvents.snsSqsEvent as SQSEvent; - testEvent.Records.map((record) => { - record.Sns.Message = JSON.stringify({ + snsSqsTestEvent.Records[0].body = JSON.stringify({ foo: 'bar', }); + + expect(SnsSqsEnvelope.safeParse(snsSqsTestEvent, TestSchema)).toEqual({ + success: false, + error: expect.any(ZodError), + originalEvent: snsSqsTestEvent, + }); }); + it('should return error if JSON parse fails for record.body', () => { + const snsSqsTestEvent = TestEvents.snsSqsEvent as SQSEvent; - expect(() => snsEnvelope(testEvent, TestSchema)).toThrowError(); + snsSqsTestEvent.Records[0].body = 'not a json string'; + + expect(SnsSqsEnvelope.safeParse(snsSqsTestEvent, TestSchema)).toEqual({ + success: false, + error: expect.any(Error), + originalEvent: snsSqsTestEvent, + }); + }); }); +}); +describe('SnsEnvelope', () => { + describe('parse', () => { + it('should parse custom schema in envelope', () => { + const testEvent = TestEvents.snsEvent as SNSEvent; - it('should parse sqs inside sns envelope', () => { - const snsSqsTestEvent = TestEvents.snsSqsEvent as SQSEvent; + const testRecords = [] as z.infer[]; - const data = generateMock(TestSchema); - const snsEvent = JSON.parse(snsSqsTestEvent.Records[0].body); - snsEvent.Message = JSON.stringify(data); + testEvent.Records.map((record) => { + const value = generateMock(TestSchema); + testRecords.push(value); + record.Sns.Message = JSON.stringify(value); + }); - snsSqsTestEvent.Records[0].body = JSON.stringify(snsEvent); + expect(SnsEnvelope.parse(testEvent, TestSchema)).toEqual(testRecords); + }); + + it('should throw if message does not macht schema', () => { + const testEvent = TestEvents.snsEvent as SNSEvent; + + testEvent.Records.map((record) => { + record.Sns.Message = JSON.stringify({ + foo: 'bar', + }); + }); + + expect(() => SnsEnvelope.parse(testEvent, TestSchema)).toThrow(); + }); + it('should throw if envelope is not valid', () => { + expect(() => SnsEnvelope.parse({ foo: 'bar' }, TestSchema)).toThrow(); + }); + }); + describe('safeParse', () => { + it('should parse custom schema in envelope', () => { + const testEvent = TestEvents.snsEvent as SNSEvent; + + const testRecords = [] as z.infer[]; + + testEvent.Records.map((record) => { + const value = generateMock(TestSchema); + testRecords.push(value); + record.Sns.Message = JSON.stringify(value); + }); - expect(snsSqsEnvelope(snsSqsTestEvent, TestSchema)).toEqual([data]); + expect(SnsEnvelope.safeParse(testEvent, TestSchema)).toEqual({ + success: true, + data: testRecords, + }); + }); + + it('should return error when message does not macht schema', () => { + const testEvent = TestEvents.snsEvent as SNSEvent; + + testEvent.Records.map((record) => { + record.Sns.Message = JSON.stringify({ + foo: 'bar', + }); + }); + + expect(SnsEnvelope.safeParse(testEvent, TestSchema)).toEqual({ + success: false, + error: expect.any(ZodError), + originalEvent: testEvent, + }); + }); + it('should return error when envelope is not valid', () => { + expect(SnsEnvelope.safeParse({ foo: 'bar' }, TestSchema)).toEqual({ + success: false, + error: expect.any(Error), + originalEvent: { foo: 'bar' }, + }); + }); }); }); diff --git a/packages/parser/tests/unit/envelopes/sqs.test.ts b/packages/parser/tests/unit/envelopes/sqs.test.ts index 7725e1a992..4d1ab3a419 100644 --- a/packages/parser/tests/unit/envelopes/sqs.test.ts +++ b/packages/parser/tests/unit/envelopes/sqs.test.ts @@ -7,39 +7,64 @@ import { generateMock } from '@anatine/zod-mock'; import { TestEvents, TestSchema } from '../schema/utils.js'; import { SQSEvent } from 'aws-lambda'; -import { sqsEnvelope } from '../../../src/envelopes/'; +import { SqsEnvelope } from '../../../src/envelopes/sqs.js'; +import { ZodError } from 'zod'; describe('SqsEnvelope ', () => { - it('should parse custom schema in envelope', () => { - const mock = generateMock(TestSchema); + describe('parse', () => { + it('should parse custom schema in envelope', () => { + const mock = generateMock(TestSchema); - const sqsEvent = TestEvents.sqsEvent as SQSEvent; - sqsEvent.Records[0].body = JSON.stringify(mock); - sqsEvent.Records[1].body = JSON.stringify(mock); + const sqsEvent = TestEvents.sqsEvent as SQSEvent; + sqsEvent.Records[0].body = JSON.stringify(mock); + sqsEvent.Records[1].body = JSON.stringify(mock); - const resp = sqsEnvelope(sqsEvent, TestSchema); - expect(resp).toEqual([mock, mock]); - }); + const resp = SqsEnvelope.parse(sqsEvent, TestSchema); + expect(resp).toEqual([mock, mock]); + }); + + it('should throw error if invalid keys for a schema', () => { + expect(() => { + SqsEnvelope.parse({ Records: [{ foo: 'bar' }] }, TestSchema); + }).toThrow(); + }); - it('should throw error if invalid keys for a schema', () => { - expect(() => { - sqsEnvelope({ Records: [{ foo: 'bar' }] }, TestSchema); - }).toThrow(); + it('should throw if invalid envelope', () => { + expect(() => { + SqsEnvelope.parse({ foo: 'bar' }, TestSchema); + }).toThrow(); + }); }); + describe('safeParse', () => { + it('should parse custom schema in envelope', () => { + const mock = generateMock(TestSchema); + + const sqsEvent = TestEvents.sqsEvent as SQSEvent; + sqsEvent.Records[0].body = JSON.stringify(mock); + sqsEvent.Records[1].body = JSON.stringify(mock); + + expect(SqsEnvelope.safeParse(sqsEvent, TestSchema)).toEqual({ + success: true, + data: [mock, mock], + }); + }); + + it('should return error if event does not match schema', () => { + const sqsEvent = TestEvents.sqsEvent as SQSEvent; + sqsEvent.Records[0].body = JSON.stringify({ foo: 'bar' }); + expect(SqsEnvelope.safeParse(sqsEvent, TestSchema)).toEqual({ + success: false, + error: expect.any(ZodError), + originalEvent: sqsEvent, + }); + }); - it('should throw error if invalid values for a schema', () => { - expect(() => { - sqsEnvelope( - { - Records: [ - { - name: 'foo', - age: 17, - }, - ], - }, - TestSchema - ); - }).toThrow(); + it('should return error if envelope is invalid', () => { + expect(SqsEnvelope.safeParse({ foo: 'bar' }, TestSchema)).toEqual({ + success: false, + error: expect.any(ZodError), + originalEvent: { foo: 'bar' }, + }); + }); }); }); diff --git a/packages/parser/tests/unit/envelopes/vpc-lattice.test.ts b/packages/parser/tests/unit/envelopes/vpc-lattice.test.ts index 7ce781100e..a4282064d2 100644 --- a/packages/parser/tests/unit/envelopes/vpc-lattice.test.ts +++ b/packages/parser/tests/unit/envelopes/vpc-lattice.test.ts @@ -6,33 +6,92 @@ import { generateMock } from '@anatine/zod-mock'; import { TestEvents, TestSchema } from '../schema/utils.js'; -import { VpcLatticeSchema } from '../../../src/schemas/'; -import { z } from 'zod'; -import { vpcLatticeEnvelope } from '../../../src/envelopes/'; +import { VpcLatticeEnvelope } from '../../../src/envelopes/index.js'; +import { VpcLatticeEvent } from '../../../src/types/index.js'; -describe('VPC Lattice envelope', () => { - it('should parse VPC Lattice event', () => { - const mock = generateMock(TestSchema); - const testEvent = TestEvents.vpcLatticeEvent as z.infer< - typeof VpcLatticeSchema - >; +describe('VpcLatticeEnvelope', () => { + describe('parse', () => { + it('should parse VPC Lattice event', () => { + const mock = generateMock(TestSchema); + const testEvent = TestEvents.vpcLatticeEvent as VpcLatticeEvent; - testEvent.body = JSON.stringify(mock); + testEvent.body = JSON.stringify(mock); - const resp = vpcLatticeEnvelope(testEvent, TestSchema); + const resp = VpcLatticeEnvelope.parse(testEvent, TestSchema); - expect(resp).toEqual(mock); + expect(resp).toEqual(mock); + }); + + it('should parse VPC Lattice event with trailing slash', () => { + const mock = generateMock(TestSchema); + const testEvent = + TestEvents.vpcLatticeEventPathTrailingSlash as VpcLatticeEvent; + + testEvent.body = JSON.stringify(mock); + + const resp = VpcLatticeEnvelope.parse(testEvent, TestSchema); + expect(resp).toEqual(mock); + }); + + it('should throw if event is not a VPC Lattice event', () => { + expect(() => + VpcLatticeEnvelope.parse({ foo: 'bar' }, TestSchema) + ).toThrow(); + }); + + it('should throw if body does not match schema', () => { + const testEvent = TestEvents.vpcLatticeEvent as VpcLatticeEvent; + + testEvent.body = JSON.stringify({ foo: 'bar' }); + + expect(() => VpcLatticeEnvelope.parse(testEvent, TestSchema)).toThrow(); + }); }); - it('should parse VPC Lattice event with trailing slash', () => { - const mock = generateMock(TestSchema); - const testEvent = TestEvents.vpcLatticeEventPathTrailingSlash as z.infer< - typeof VpcLatticeSchema - >; + describe('safeParse', () => { + it('should parse VPC Lattice event', () => { + const mock = generateMock(TestSchema); + const testEvent = TestEvents.vpcLatticeEvent as VpcLatticeEvent; + + testEvent.body = JSON.stringify(mock); + + const resp = VpcLatticeEnvelope.safeParse(testEvent, TestSchema); + + expect(resp).toEqual({ success: true, data: mock }); + }); + + it('should parse VPC Lattice event with trailing slash', () => { + const mock = generateMock(TestSchema); + const testEvent = + TestEvents.vpcLatticeEventPathTrailingSlash as VpcLatticeEvent; + + testEvent.body = JSON.stringify(mock); + + const resp = VpcLatticeEnvelope.safeParse(testEvent, TestSchema); + expect(resp).toEqual({ success: true, data: mock }); + }); + + it('should return error if event is not a VPC Lattice event', () => { + const resp = VpcLatticeEnvelope.safeParse({ foo: 'bar' }, TestSchema); + + expect(resp).toEqual({ + success: false, + error: expect.any(Error), + originalEvent: { foo: 'bar' }, + }); + }); + + it('should return error if body does not match schema', () => { + const testEvent = TestEvents.vpcLatticeEvent as VpcLatticeEvent; - testEvent.body = JSON.stringify(mock); + testEvent.body = JSON.stringify({ foo: 'bar' }); - const resp = vpcLatticeEnvelope(testEvent, TestSchema); - expect(resp).toEqual(mock); + const resp = VpcLatticeEnvelope.safeParse(testEvent, TestSchema); + expect(resp).toEqual({ + success: false, + error: expect.any(Error), + originalEvent: testEvent, + }); + }); }); }); diff --git a/packages/parser/tests/unit/envelopes/vpc-latticev2.test.ts b/packages/parser/tests/unit/envelopes/vpc-latticev2.test.ts index 251382fb87..6f615178ec 100644 --- a/packages/parser/tests/unit/envelopes/vpc-latticev2.test.ts +++ b/packages/parser/tests/unit/envelopes/vpc-latticev2.test.ts @@ -5,34 +5,93 @@ */ import { generateMock } from '@anatine/zod-mock'; -import { VpcLatticeSchema } from '../../../src/schemas/'; -import { z } from 'zod'; import { TestEvents, TestSchema } from '../schema/utils.js'; -import { vpcLatticeV2Envelope } from '../../../src/envelopes/'; +import { VpcLatticeV2Envelope } from '../../../src/envelopes/index.js'; +import { VpcLatticeEventV2 } from '../../../src/types/index.js'; -describe('VPC Lattice envelope', () => { - it('should parse VPC Lattice event', () => { - const mock = generateMock(TestSchema); - const testEvent = TestEvents.vpcLatticeV2Event as z.infer< - typeof VpcLatticeSchema - >; +describe('VpcLatticeV2Envelope2', () => { + describe('parse', () => { + it('should parse VPC Lattice event', () => { + const mock = generateMock(TestSchema); + const testEvent = TestEvents.vpcLatticeV2Event as VpcLatticeEventV2; - testEvent.body = JSON.stringify(mock); + testEvent.body = JSON.stringify(mock); - const resp = vpcLatticeV2Envelope(testEvent, TestSchema); + const resp = VpcLatticeV2Envelope.parse(testEvent, TestSchema); - expect(resp).toEqual(mock); + expect(resp).toEqual(mock); + }); + + it('should parse VPC Lattice event with trailing slash', () => { + const mock = generateMock(TestSchema); + const testEvent = + TestEvents.vpcLatticeEventV2PathTrailingSlash as VpcLatticeEventV2; + + testEvent.body = JSON.stringify(mock); + + const resp = VpcLatticeV2Envelope.parse(testEvent, TestSchema); + expect(resp).toEqual(mock); + }); + + it('should throw if event is not a VPC Lattice event', () => { + expect(() => + VpcLatticeV2Envelope.parse({ foo: 'bar' }, TestSchema) + ).toThrow(); + }); + + it('should throw if body does not match schema', () => { + const testEvent = TestEvents.vpcLatticeV2Event as VpcLatticeEventV2; + + testEvent.body = JSON.stringify({ foo: 'bar' }); + + expect(() => VpcLatticeV2Envelope.parse(testEvent, TestSchema)).toThrow(); + }); }); - it('should parse VPC Lattice event with trailing slash', () => { - const mock = generateMock(TestSchema); - const testEvent = TestEvents.vpcLatticeEventV2PathTrailingSlash as z.infer< - typeof VpcLatticeSchema - >; + describe('safeParse', () => { + it('should parse VPC Lattice event', () => { + const mock = generateMock(TestSchema); + const testEvent = TestEvents.vpcLatticeV2Event as VpcLatticeEventV2; + + testEvent.body = JSON.stringify(mock); + + const resp = VpcLatticeV2Envelope.safeParse(testEvent, TestSchema); + + expect(resp).toEqual({ success: true, data: mock }); + }); + + it('should parse VPC Lattice event with trailing slash', () => { + const mock = generateMock(TestSchema); + const testEvent = + TestEvents.vpcLatticeEventV2PathTrailingSlash as VpcLatticeEventV2; + + testEvent.body = JSON.stringify(mock); + + const resp = VpcLatticeV2Envelope.safeParse(testEvent, TestSchema); + expect(resp).toEqual({ success: true, data: mock }); + }); + + it('should return error if event is not a VPC Lattice event', () => { + const resp = VpcLatticeV2Envelope.safeParse({ foo: 'bar' }, TestSchema); + + expect(resp).toEqual({ + success: false, + error: expect.any(Error), + originalEvent: { foo: 'bar' }, + }); + }); + + it('should return error if body does not match schema', () => { + const testEvent = TestEvents.vpcLatticeV2Event as VpcLatticeEventV2; - testEvent.body = JSON.stringify(mock); + testEvent.body = JSON.stringify({ foo: 'bar' }); - const resp = vpcLatticeV2Envelope(testEvent, TestSchema); - expect(resp).toEqual(mock); + const resp = VpcLatticeV2Envelope.safeParse(testEvent, TestSchema); + expect(resp).toEqual({ + success: false, + error: expect.any(Error), + originalEvent: testEvent, + }); + }); }); }); diff --git a/packages/parser/tests/unit/parser.decorator.test.ts b/packages/parser/tests/unit/parser.decorator.test.ts index 11df5d6703..1e05efa510 100644 --- a/packages/parser/tests/unit/parser.decorator.test.ts +++ b/packages/parser/tests/unit/parser.decorator.test.ts @@ -5,25 +5,26 @@ */ import type { LambdaInterface } from '@aws-lambda-powertools/commons/lib/esm/types'; -import { Context, EventBridgeEvent } from 'aws-lambda'; +import { Context } from 'aws-lambda'; import { parser } from '../../src/index.js'; import { TestSchema, TestEvents } from './schema/utils'; import { generateMock } from '@anatine/zod-mock'; -import { eventBridgeEnvelope } from '../../src/envelopes/index.js'; import { EventBridgeSchema } from '../../src/schemas/index.js'; -import { z } from 'zod'; +import { z, ZodError } from 'zod'; +import { ParsedResult, EventBridgeEvent } from '../../src/types'; +import { EventBridgeEnvelope } from '../../src/envelopes/index.js'; describe('Parser Decorator', () => { const customEventBridgeSchema = EventBridgeSchema.extend({ detail: TestSchema, }); - type TestSchema = z.infer; + type TestEvent = z.infer; class TestClass implements LambdaInterface { @parser({ schema: TestSchema }) public async handler( - event: TestSchema, + event: TestEvent, _context: Context ): Promise { return event; @@ -37,23 +38,46 @@ describe('Parser Decorator', () => { return event; } - @parser({ schema: TestSchema, envelope: eventBridgeEnvelope }) + @parser({ schema: TestSchema, envelope: EventBridgeEnvelope }) public async handlerWithParserCallsAnotherMethod( - event: unknown, + event: TestEvent, _context: Context ): Promise { - return this.anotherMethod(event as TestSchema); + return this.anotherMethod(event); } - @parser({ envelope: eventBridgeEnvelope, schema: TestSchema }) + @parser({ schema: TestSchema, envelope: EventBridgeEnvelope }) public async handlerWithSchemaAndEnvelope( - event: unknown, + event: TestEvent, _context: Context ): Promise { return event; } - private async anotherMethod(event: TestSchema): Promise { + @parser({ + schema: TestSchema, + safeParse: true, + }) + public async handlerWithSchemaAndSafeParse( + event: ParsedResult, + _context: Context + ): Promise { + return event; + } + + @parser({ + schema: TestSchema, + envelope: EventBridgeEnvelope, + safeParse: true, + }) + public async harndlerWithEnvelopeAndSafeParse( + event: ParsedResult, + _context: Context + ): Promise { + return event; + } + + private async anotherMethod(event: TestEvent): Promise { return event; } } @@ -70,13 +94,12 @@ describe('Parser Decorator', () => { it('should parse custom schema with envelope event', async () => { const customPayload = generateMock(TestSchema); - const testEvent = TestEvents.eventBridgeEvent as EventBridgeEvent< - string, - unknown - >; + const testEvent = TestEvents.eventBridgeEvent as EventBridgeEvent; testEvent.detail = customPayload; const resp = await lambda.handlerWithSchemaAndEnvelope( + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore testEvent, {} as Context ); @@ -102,17 +125,77 @@ describe('Parser Decorator', () => { it('should parse and call private async method', async () => { const customPayload = generateMock(TestSchema); - const testEvent = TestEvents.eventBridgeEvent as EventBridgeEvent< - string, - unknown - >; + const testEvent = TestEvents.eventBridgeEvent as EventBridgeEvent; testEvent.detail = customPayload; const resp = await lambda.handlerWithParserCallsAnotherMethod( + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore testEvent, {} as Context ); expect(resp).toEqual(customPayload); }); + + it('should parse event with schema and safeParse', async () => { + const testEvent = generateMock(TestSchema); + + const resp = await lambda.handlerWithSchemaAndSafeParse( + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + testEvent, + {} as Context + ); + + expect(resp).toEqual({ + success: true, + data: testEvent, + }); + }); + + it('should parse event with schema and safeParse and return error', async () => { + expect( + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + await lambda.handlerWithSchemaAndSafeParse({ foo: 'bar' }, {} as Context) + ).toEqual({ + error: expect.any(ZodError), + success: false, + originalEvent: { foo: 'bar' }, + }); + }); + + it('should parse event with envelope and safeParse', async () => { + const testEvent = generateMock(TestSchema); + const event = TestEvents.eventBridgeEvent as EventBridgeEvent; + event.detail = testEvent; + + const resp = await lambda.harndlerWithEnvelopeAndSafeParse( + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + event, + {} as Context + ); + + expect(resp).toEqual({ + success: true, + data: testEvent, + }); + }); + + it('should parse event with envelope and safeParse and return error', async () => { + expect( + await lambda.harndlerWithEnvelopeAndSafeParse( + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + { foo: 'bar' }, + {} as Context + ) + ).toEqual({ + error: expect.any(ZodError), + success: false, + originalEvent: { foo: 'bar' }, + }); + }); }); diff --git a/packages/parser/tests/unit/parser.middy.test.ts b/packages/parser/tests/unit/parser.middy.test.ts index 369a04d776..d8dd4ae1b1 100644 --- a/packages/parser/tests/unit/parser.middy.test.ts +++ b/packages/parser/tests/unit/parser.middy.test.ts @@ -10,8 +10,9 @@ import { parser } from '../../src/middleware/parser.js'; import { generateMock } from '@anatine/zod-mock'; import { SqsSchema } from '../../src/schemas/index.js'; import { z, type ZodSchema } from 'zod'; -import { sqsEnvelope } from '../../src/envelopes/index.js'; -import { TestSchema } from './schema/utils'; +import { SqsEnvelope, EventBridgeEnvelope } from '../../src/envelopes/index.js'; +import { TestSchema, TestEvents } from './schema/utils'; +import { EventBridgeEvent } from '../../src/types/index.js'; describe('Middleware: parser', () => { type schema = z.infer; @@ -23,13 +24,13 @@ describe('Middleware: parser', () => { }; describe(' when envelope is provided ', () => { - const middyfiedHandler = middy(handler).use( - parser({ schema: TestSchema, envelope: sqsEnvelope }) + const middyfiedHandlerSchemaEnvelope = 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 }); + parser({ schema: TestSchema, envelope: SqsEnvelope }); const event = generateMock(SqsSchema, { stringMap: { @@ -37,7 +38,10 @@ describe('Middleware: parser', () => { }, }); - const result = (await middyfiedHandler(event, {} as Context)) as schema[]; + const result = (await middyfiedHandlerSchemaEnvelope( + event, + {} as Context + )) as schema[]; result.forEach((item) => { expect(item).toEqual(bodyMock); }); @@ -45,8 +49,11 @@ describe('Middleware: parser', () => { it('should throw when envelope does not match', async () => { await expect(async () => { - await middyfiedHandler({ name: 'John', age: 18 }, {} as Context); - }).rejects.toThrowError(); + await middyfiedHandlerSchemaEnvelope( + { name: 'John', age: 18 }, + {} as Context + ); + }).rejects.toThrow(); }); it('should throw when schema does not match', async () => { @@ -56,14 +63,16 @@ describe('Middleware: parser', () => { }, }); - await expect(middyfiedHandler(event, {} as Context)).rejects.toThrow(); + await expect( + middyfiedHandlerSchemaEnvelope(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 }) + parser({ schema: {} as ZodSchema, envelope: SqsEnvelope }) ); - await expect(middyfiedHandler(42, {} as Context)).rejects.toThrowError(); + await expect(middyfiedHandler(42, {} as Context)).rejects.toThrow(); }); it('should throw when envelope is correct but schema is invalid', async () => { const event = generateMock(SqsSchema, { @@ -73,12 +82,10 @@ describe('Middleware: parser', () => { }); const middyfiedHandler = middy(handler).use( - parser({ schema: {} as ZodSchema, envelope: sqsEnvelope }) + parser({ schema: {} as ZodSchema, envelope: SqsEnvelope }) ); - await expect( - middyfiedHandler(event, {} as Context) - ).rejects.toThrowError(); + await expect(middyfiedHandler(event, {} as Context)).rejects.toThrow(); }); }); @@ -117,7 +124,69 @@ describe('Middleware: parser', () => { await expect( middyfiedHandler({ foo: 'bar' }, {} as Context) - ).rejects.toThrowError(); + ).rejects.toThrow(); + }); + + it('should return the event when safeParse is true', async () => { + const event = { name: 'John', age: 18 }; + const middyfiedHandler = middy(handler).use( + parser({ schema: TestSchema, safeParse: true }) + ); + + expect(await middyfiedHandler(event, {} as Context)).toEqual({ + success: true, + data: event, + }); + }); + + it('should return error when safeParse is true and schema does not match', async () => { + const middyfiedHandler = middy(handler).use( + parser({ schema: TestSchema, safeParse: true }) + ); + + expect(await middyfiedHandler(42, {} as Context)).toEqual({ + success: false, + error: expect.any(Error), + originalEvent: 42, + }); + }); + + it('should return event when envelope and safeParse are true', async () => { + const detail = generateMock(TestSchema); + const event = TestEvents.eventBridgeEvent as EventBridgeEvent; + + event.detail = detail; + + const middyfiedHandler = middy(handler).use( + parser({ + schema: TestSchema, + envelope: EventBridgeEnvelope, + safeParse: true, + }) + ); + + expect(await middyfiedHandler(event, {} as Context)).toEqual({ + success: true, + data: detail, + }); + }); + + it('should return error when envelope and safeParse are true and schema does not match', async () => { + const event = TestEvents.eventBridgeEvent as EventBridgeEvent; + + const middyfiedHandler = middy(handler).use( + parser({ + schema: TestSchema, + envelope: EventBridgeEnvelope, + safeParse: true, + }) + ); + + expect(await middyfiedHandler(event, {} as Context)).toEqual({ + success: false, + error: expect.any(Error), + originalEvent: event, + }); }); }); });