Skip to content

Commit 46d5b14

Browse files
committed
Created the Base64Encoded helper to decode base64 strings and validate against schema
1 parent 8b25ce7 commit 46d5b14

File tree

2 files changed

+161
-2
lines changed

2 files changed

+161
-2
lines changed

packages/parser/src/helpers/index.ts

Lines changed: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,17 @@
1+
import { gunzipSync } from 'node:zlib';
2+
import { fromBase64 } from '@aws-lambda-powertools/commons/utils/base64';
13
import { type ZodType, z } from 'zod';
24

5+
const decoder = new TextDecoder();
6+
7+
const decompress = (data: string): string => {
8+
try {
9+
return JSON.parse(gunzipSync(fromBase64(data, 'base64')).toString('utf8'));
10+
} catch {
11+
return data;
12+
}
13+
};
14+
315
/**
416
* A helper function to parse a JSON string and validate it against a schema.
517
*
@@ -54,4 +66,53 @@ const JSONStringified = <T extends ZodType>(schema: T) =>
5466
})
5567
.pipe(schema);
5668

57-
export { JSONStringified };
69+
/**
70+
* A helper function to decode a Base64 string and validate it against a schema.
71+
*
72+
*
73+
* Use it for built-in schemas like `KinesisDataStreamRecordPayload` that have fields that are base64 encoded
74+
* and extend them with your custom schema.
75+
*
76+
* For example, if you have an event with a base64 encoded body similar to the following:
77+
*
78+
* ```json
79+
* {
80+
* // ... other fields
81+
* "data": "e3Rlc3Q6ICJ0ZXN0In0=",
82+
* }
83+
* ```
84+
*
85+
* You can extend any built-in schema with your custom schema using the `Base64Encoded` helper function.
86+
*
87+
* @example
88+
* ```typescript
89+
* import { Base64Encoded } from '@aws-lambda-powertools/parser/helpers';
90+
* import { KinesisDataStreamRecordPayload } from '@aws-lambda-powertools/parser/schemas/kinesis';
91+
* import { z } from 'zod';
92+
*
93+
* const extendedSchema = KinesisDataStreamRecordPayload.extend({
94+
* data: Base64Encoded(z.object({
95+
* test: z.string(),
96+
* }))
97+
* });
98+
* type _ExtendedKinesisDataStream = z.infer<typeof extendedSchema>;
99+
* ```
100+
*
101+
* @param schema - The schema to validate the Base 64 decoded value against
102+
*/
103+
const Base64Encoded = <T extends ZodType>(schema: T) =>
104+
z
105+
.string()
106+
.transform((data) => {
107+
const decompressed = decompress(data);
108+
const decoded = decoder.decode(fromBase64(data, 'base64'));
109+
try {
110+
// If data was not compressed, try to parse it as JSON otherwise it must be string
111+
return decompressed === data ? JSON.parse(decoded) : decompressed;
112+
} catch {
113+
return decoded;
114+
}
115+
})
116+
.pipe(schema);
117+
118+
export { JSONStringified, Base64Encoded };

packages/parser/tests/unit/helpers.test.ts

Lines changed: 99 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
1+
import {
2+
KinesisDataStreamRecord,
3+
KinesisDataStreamRecordPayload,
4+
KinesisDataStreamSchema,
5+
} from 'src/schemas/kinesis.js';
16
import { describe, expect, it } from 'vitest';
27
import { z } from 'zod';
38
import { DynamoDBMarshalled } from '../../src/helpers/dynamodb.js';
4-
import { JSONStringified } from '../../src/helpers/index.js';
9+
import { Base64Encoded, JSONStringified } from '../../src/helpers/index.js';
510
import { AlbSchema } from '../../src/schemas/alb.js';
611
import {
712
DynamoDBStreamRecord,
@@ -14,6 +19,7 @@ import {
1419
import { SqsRecordSchema, SqsSchema } from '../../src/schemas/sqs.js';
1520
import type {
1621
DynamoDBStreamEvent,
22+
KinesisDataStreamEvent,
1723
SnsEvent,
1824
SqsEvent,
1925
} from '../../src/types/schema.js';
@@ -277,3 +283,95 @@ describe('Helper: DynamoDBMarshalled', () => {
277283
expect(() => extendedSchema.parse(event)).toThrow();
278284
});
279285
});
286+
287+
describe('Helper: Base64Encoded', () => {
288+
it('returns a valid base64 decoded payload', () => {
289+
// Prepare
290+
const data = {
291+
body: Buffer.from(JSON.stringify(structuredClone(basePayload))).toString(
292+
'base64'
293+
),
294+
};
295+
296+
// Act
297+
const extendedSchema = envelopeSchema.extend({
298+
body: Base64Encoded(bodySchema),
299+
});
300+
301+
// Assess
302+
expect(extendedSchema.parse(data)).toStrictEqual({
303+
body: basePayload,
304+
});
305+
});
306+
307+
it('throws an error if the payload is invalid', () => {
308+
// Prepare
309+
const data = {
310+
body: Buffer.from(
311+
JSON.stringify({ ...basePayload, email: 'invalid' })
312+
).toString('base64'),
313+
};
314+
315+
// Act
316+
const extendedSchema = envelopeSchema.extend({
317+
body: Base64Encoded(bodySchema),
318+
});
319+
320+
// Assess
321+
expect(() => extendedSchema.parse(data)).toThrow();
322+
});
323+
324+
it('throws an error if the base64 payload is malformed', () => {
325+
// Prepare
326+
const data = {
327+
body: 'invalid-base64-string',
328+
};
329+
330+
// Act
331+
const extendedSchema = envelopeSchema.extend({
332+
body: Base64Encoded(bodySchema),
333+
});
334+
335+
// Assess
336+
expect(() => extendedSchema.parse(data)).toThrow();
337+
});
338+
339+
it('parses extended KinesisDataStreamSchema', () => {
340+
// Prepare
341+
const testEvent = getTestEvent<KinesisDataStreamEvent>({
342+
eventsPath: 'kinesis',
343+
filename: 'stream',
344+
});
345+
const stringifiedBody = JSON.stringify(basePayload);
346+
testEvent.Records[0].kinesis.data =
347+
Buffer.from(stringifiedBody).toString('base64');
348+
testEvent.Records[1].kinesis.data =
349+
Buffer.from(stringifiedBody).toString('base64');
350+
351+
// Act
352+
const extendedSchema = KinesisDataStreamSchema.extend({
353+
Records: z.array(
354+
KinesisDataStreamRecord.extend({
355+
kinesis: KinesisDataStreamRecordPayload.extend({
356+
data: Base64Encoded(bodySchema),
357+
}),
358+
})
359+
),
360+
});
361+
362+
// Assess
363+
expect(extendedSchema.parse(testEvent)).toStrictEqual({
364+
...testEvent,
365+
Records: [
366+
{
367+
...testEvent.Records[0],
368+
kinesis: { ...testEvent.Records[0].kinesis, data: basePayload },
369+
},
370+
{
371+
...testEvent.Records[1],
372+
kinesis: { ...testEvent.Records[1].kinesis, data: basePayload },
373+
},
374+
],
375+
});
376+
});
377+
});

0 commit comments

Comments
 (0)