@middy/event-normalizer
Version: 
Parse and normalize AWS events middleware for the middy framework
196 lines (183 loc) • 5.36 kB
JavaScript
import { jsonSafeParse } from "@middy/util";
const defaults = {
	wrapNumbers: undefined,
};
let _wrapNumbers;
const eventNormalizerMiddleware = (opts = {}) => {
	const { wrapNumbers } = { ...defaults, ...opts };
	_wrapNumbers = wrapNumbers;
	const eventNormalizerMiddlewareBefore = async (request) => {
		parseEvent(request.event);
	};
	return {
		before: eventNormalizerMiddlewareBefore,
	};
};
const parseEvent = (event) => {
	// event.eventSource => aws:amq, aws:docdb, aws:kafka, SelfManagedKafka
	// event.deliveryStreamArn => aws:lambda:events
	let eventSource = event.eventSource ?? event.deliveryStreamArn;
	// event.Records => default
	// event.records => aws:lambda:events
	// event.messages => aws:amq
	// event.tasks => aws:s3:batch
	// event.events => aws:docdb
	const records =
		event.Records ??
		event.records ??
		event.messages ??
		event.tasks ??
		event.events;
	if (!Array.isArray(records)) {
		// event.configRuleId => aws:config
		// event.awslogs => aws:cloudwatch
		// event['CodePipeline.job'] => aws:codepipeline
		eventSource ??=
			(event.configRuleId && "aws:config") ??
			(event.awslogs && "aws:cloudwatch") ??
			(event["CodePipeline.job"] && "aws:codepipeline");
		if (eventSource) {
			events[eventSource]?.(event);
		}
		return;
	}
	// record.eventSource => default
	// record.EventSource => aws:sns
	// record.s3Key => aws:s3:batch
	eventSource ??=
		records[0].eventSource ??
		records[0].EventSource ??
		(records[0].s3Key && "aws:s3:batch");
	for (const record of records) {
		events[eventSource]?.(record);
	}
};
const normalizeS3KeyReplacePlus = /\+/g;
const events = {
	"aws:amq": (message) => {
		message.data = base64Parse(message.data);
	},
	"aws:cloudwatch": (event) => {
		event.awslogs.data = base64Parse(event.awslogs.data);
	},
	"aws:codepipeline": (event) => {
		event[
			"CodePipeline.job"
		].data.actionConfiguration.configuration.UserParameters = jsonSafeParse(
			event["CodePipeline.job"].data.actionConfiguration.configuration
				.UserParameters,
		);
	},
	"aws:config": (event) => {
		event.invokingEvent = jsonSafeParse(event.invokingEvent);
		event.ruleParameters = jsonSafeParse(event.ruleParameters);
	},
	// 'aws:docdb': (record) => {},
	"aws:dynamodb": (record) => {
		record.dynamodb.Keys = unmarshall(record.dynamodb.Keys);
		record.dynamodb.NewImage = unmarshall(record.dynamodb.NewImage);
		record.dynamodb.OldImage = unmarshall(record.dynamodb.OldImage);
	},
	"aws:kafka": (event) => {
		for (const record in event.records) {
			for (const topic of event.records[record]) {
				topic.value &&= base64Parse(topic.value);
			}
		}
	},
	// Kinesis Stream
	"aws:kinesis": (record) => {
		record.kinesis.data = base64Parse(record.kinesis.data);
	},
	// Kinesis Firehose
	"aws:lambda:events": (record) => {
		record.data = base64Parse(record.data);
	},
	"aws:s3": (record) => {
		record.s3.object.key = normalizeS3Key(record.s3.object.key);
	},
	"aws:s3:batch": (task) => {
		task.s3Key = normalizeS3Key(task.s3Key);
	},
	SelfManagedKafka: (event) => {
		events["aws:kafka"](event);
	},
	"aws:sns": (record) => {
		record.Sns.Message = jsonSafeParse(record.Sns.Message);
		parseEvent(record.Sns.Message);
	},
	"aws:sns:sqs": (record) => {
		record.Message = jsonSafeParse(record.Message);
		parseEvent(record.Message);
	},
	"aws:sqs": (record) => {
		record.body = jsonSafeParse(record.body);
		// SNS -> SQS Special Case
		if (record.body.Type === "Notification") {
			events["aws:sns:sqs"](record.body);
		} else {
			parseEvent(record.body);
		}
	},
};
const base64Parse = (data) =>
	jsonSafeParse(Buffer.from(data, "base64").toString("utf-8"));
const normalizeS3Key = (key) =>
	decodeURIComponent(key.replace(normalizeS3KeyReplacePlus, " ")); // decodeURIComponent(key.replaceAll('+', ' '))
// Start: AWS SDK unmarshall
// Reference: https://github.com/aws/aws-sdk-js-v3/blob/v3.113.0/packages/util-dynamodb/src/convertToNative.ts
const unmarshall = (data) => convertValue.M(data ?? {});
const convertValue = {
	NULL: () => null,
	BOOL: Boolean,
	N: (value) => {
		if (_wrapNumbers) {
			return { value };
		}
		const num = Number(value);
		if (
			(Number.MAX_SAFE_INTEGER < num || num < Number.MIN_SAFE_INTEGER) &&
			num !== Number.NEGATIVE_INFINITY &&
			num !== Number.POSITIVE_INFINITY
		) {
			try {
				return BigInt(value);
			} catch (e) {
				throw new Error(
					`${value} can't be converted to BigInt. Set options.wrapNumbers to get string value.`,
					{
						cause: {
							package: "@middy/event-normalizer",
							e,
						},
					},
				);
			}
		}
		return num;
	},
	B: (value) => value,
	S: (value) => value,
	L: (value) => value.map((item) => convertToNative(item)),
	M: (value) =>
		Object.entries(value).reduce((acc, [key, value]) => {
			acc[key] = convertToNative(value);
			return acc;
		}, {}),
	NS: (value) => new Set(value.map(convertValue.N)),
	BS: (value) => new Set(value.map(convertValue.B)),
	SS: (value) => new Set(value.map(convertValue.S)),
};
const convertToNative = (data) => {
	for (const [key, value] of Object.entries(data)) {
		if (!convertValue[key]) {
			throw new Error(`Unsupported type passed: ${key}`, {
				cause: { package: "@middy/event-normalizer" },
			});
		}
		if (typeof value === "undefined") continue;
		return convertValue[key](value);
	}
};
// End: AWS SDK unmarshall
export default eventNormalizerMiddleware;