UNPKG

@middy/event-normalizer

Version:

Parse and normalize AWS events middleware for the middy framework

198 lines (185 loc) 5.66 kB
// Copyright 2017 - 2026 will Farrell, Luciano Mammino, and Middy contributors. // SPDX-License-Identifier: MIT import { jsonSafeParse } from "@middy/util"; const defaults = { wrapNumbers: undefined, }; const eventNormalizerMiddleware = (opts = {}) => { const options = { ...defaults, ...opts }; const eventNormalizerMiddlewareBefore = (request) => { parseEvent(request.event, options); }; return { before: eventNormalizerMiddlewareBefore, }; }; const parseEvent = (event, options) => { // 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, options); } 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, options); } }; 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, options) => { record.dynamodb.Keys = unmarshall(record.dynamodb.Keys, options); record.dynamodb.NewImage = unmarshall(record.dynamodb.NewImage, options); record.dynamodb.OldImage = unmarshall(record.dynamodb.OldImage, options); }, "aws:kafka": (event) => { for (const topics of Object.values(event.records)) { for (const topic of topics) { 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, options) => { record.Sns.Message = jsonSafeParse(record.Sns.Message); parseEvent(record.Sns.Message, options); }, "aws:sns:sqs": (record, options) => { record.Message = jsonSafeParse(record.Message); parseEvent(record.Message, options); }, "aws:sqs": (record, options) => { record.body = jsonSafeParse(record.body); // SNS -> SQS Special Case if (record.body.Type === "Notification") { events["aws:sns:sqs"](record.body, options); } else { parseEvent(record.body, options); } }, }; 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, options) => convertValue.M(data ?? {}, options); const convertValue = { NULL: () => null, BOOL: Boolean, N: (value, options) => { if (options.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 (_err) { throw new Error( `${value} can't be converted to BigInt. Set options.wrapNumbers to get string value.`, { cause: { package: "@middy/event-normalizer", value, }, }, ); } } return num; }, B: (value) => value, S: (value) => value, L: (value, options) => value.map((item) => convertToNative(item, options)), M: (value, options) => { const obj = Object.create(null); for (const key in value) { obj[key] = convertToNative(value[key], options); } return obj; }, NS: (value, options) => new Set(value.map((v) => convertValue.N(v, options))), BS: (value) => new Set(value.map(convertValue.B)), SS: (value) => new Set(value.map(convertValue.S)), }; const convertToNative = (data, options) => { for (const key in data) { if (!convertValue[key]) { throw new Error(`Unsupported type passed: ${key}`, { cause: { package: "@middy/event-normalizer" }, }); } if (typeof data[key] === "undefined") continue; return convertValue[key](data[key], options); } }; // End: AWS SDK unmarshall export default eventNormalizerMiddleware;