Skip to content

Commit ce9f8a1

Browse files
authored
feat(parser): add schema envelopes (#1815)
* first envelope * add abstract class * add tests * add more tests * fix tests * add envelopes * add middy parser * minor schema changes * add more envelopes and tests, refactored utils to autocomplete event files * simplified check * remove middleware from this branch * refactored from class to function envelopes * removed parser tests, should be in another branch * add parser to pre push * consistent naming
1 parent 20cde95 commit ce9f8a1

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

53 files changed

+1241
-173
lines changed

Diff for: .husky/pre-push

+2-1
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,5 @@ npm t \
77
-w packages/metrics \
88
-w packages/tracer \
99
-w packages/idempotency \
10-
-w packages/parameters
10+
-w packages/parameters \
11+
-w packages/parser

Diff for: package-lock.json

+65-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Diff for: packages/parser/package.json

+5-2
Original file line numberDiff line numberDiff line change
@@ -60,8 +60,11 @@
6060
"serverless",
6161
"nodejs"
6262
],
63-
6463
"peerDependencies": {
6564
"zod": ">=3.x"
65+
},
66+
"devDependencies": {
67+
"@anatine/zod-mock": "^3.13.3",
68+
"@faker-js/faker": "^8.3.1"
6669
}
67-
}
70+
}

Diff for: packages/parser/src/envelopes/apigw.ts

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { parse } from './envelope.js';
2+
import { z, ZodSchema } from 'zod';
3+
import { APIGatewayProxyEventSchema } from '../schemas/apigw.js';
4+
5+
/**
6+
* API Gateway envelope to extract data within body key
7+
*/
8+
export const apiGatewayEnvelope = <T extends ZodSchema>(
9+
data: unknown,
10+
schema: T
11+
): z.infer<T> => {
12+
const parsedEnvelope = APIGatewayProxyEventSchema.parse(data);
13+
if (!parsedEnvelope.body) {
14+
throw new Error('Body field of API Gateway event is undefined');
15+
}
16+
17+
return parse(parsedEnvelope.body, schema);
18+
};

Diff for: packages/parser/src/envelopes/apigwv2.ts

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { parse } from './envelope.js';
2+
import { z, ZodSchema } from 'zod';
3+
import { APIGatewayProxyEventV2Schema } from '../schemas/apigwv2.js';
4+
5+
/**
6+
* API Gateway V2 envelope to extract data within body key
7+
*/
8+
export const apiGatewayV2Envelope = <T extends ZodSchema>(
9+
data: unknown,
10+
schema: T
11+
): z.infer<T> => {
12+
const parsedEnvelope = APIGatewayProxyEventV2Schema.parse(data);
13+
if (!parsedEnvelope.body) {
14+
throw new Error('Body field of API Gateway event is undefined');
15+
}
16+
17+
return parse(parsedEnvelope.body, schema);
18+
};

Diff for: packages/parser/src/envelopes/cloudwatch.ts

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { parse } from './envelope.js';
2+
import { z, ZodSchema } from 'zod';
3+
import { CloudWatchLogsSchema } from '../schemas/cloudwatch.js';
4+
5+
/**
6+
* CloudWatch Envelope to extract a List of log records.
7+
*
8+
* The record's body parameter is a string (after being base64 decoded and gzipped),
9+
* though it can also be a JSON encoded string.
10+
* Regardless of its type it'll be parsed into a BaseModel object.
11+
*
12+
* Note: The record will be parsed the same way so if model is str
13+
*/
14+
export const cloudWatchEnvelope = <T extends ZodSchema>(
15+
data: unknown,
16+
schema: T
17+
): z.infer<T> => {
18+
const parsedEnvelope = CloudWatchLogsSchema.parse(data);
19+
20+
return parsedEnvelope.awslogs.data.logEvents.map((record) => {
21+
return parse(record.message, schema);
22+
});
23+
};

Diff for: packages/parser/src/envelopes/dynamodb.ts

+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { parse } from './envelope.js';
2+
import { z, ZodSchema } from 'zod';
3+
import { DynamoDBStreamSchema } from '../schemas/dynamodb.js';
4+
5+
type DynamoDBStreamEnvelopeResponse<T extends ZodSchema> = {
6+
NewImage: z.infer<T>;
7+
OldImage: z.infer<T>;
8+
};
9+
10+
/**
11+
* DynamoDB Stream Envelope to extract data within NewImage/OldImage
12+
*
13+
* Note: Values are the parsed models. Images' values can also be None, and
14+
* length of the list is the record's amount in the original event.
15+
*/
16+
export const dynamoDDStreamEnvelope = <T extends ZodSchema>(
17+
data: unknown,
18+
schema: T
19+
): DynamoDBStreamEnvelopeResponse<T>[] => {
20+
const parsedEnvelope = DynamoDBStreamSchema.parse(data);
21+
22+
return parsedEnvelope.Records.map((record) => {
23+
return {
24+
NewImage: parse(record.dynamodb.NewImage, schema),
25+
OldImage: parse(record.dynamodb.OldImage, schema),
26+
};
27+
});
28+
};

Diff for: packages/parser/src/envelopes/envelope.ts

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { z, ZodSchema } from 'zod';
2+
3+
/**
4+
* Abstract function to parse the content of the envelope using provided schema.
5+
* Both inputs are provided as unknown by the user.
6+
* We expect the data to be either string that can be parsed to json or object.
7+
* @internal
8+
* @param data data to parse
9+
* @param schema schema
10+
*/
11+
export const parse = <T extends ZodSchema>(
12+
data: unknown,
13+
schema: T
14+
): z.infer<T>[] => {
15+
if (typeof data === 'string') {
16+
return schema.parse(JSON.parse(data));
17+
} else if (typeof data === 'object') {
18+
return schema.parse(data);
19+
} else
20+
throw new Error(
21+
`Invalid data type for envelope. Expected string or object, got ${typeof data}`
22+
);
23+
};

Diff for: packages/parser/src/envelopes/eventbridge.ts

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { parse } from './envelope.js';
2+
import { z, ZodSchema } from 'zod';
3+
import { EventBridgeSchema } from '../schemas/eventbridge.js';
4+
5+
/**
6+
* Envelope for EventBridge schema that extracts and parses data from the `detail` key.
7+
*/
8+
export const eventBridgeEnvelope = <T extends ZodSchema>(
9+
data: unknown,
10+
schema: T
11+
): z.infer<T> => {
12+
return parse(EventBridgeSchema.parse(data).detail, schema);
13+
};

Diff for: packages/parser/src/envelopes/kafka.ts

+39
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { z, ZodSchema } from 'zod';
2+
import { parse } from './envelope.js';
3+
import {
4+
KafkaMskEventSchema,
5+
KafkaSelfManagedEventSchema,
6+
} from '../schemas/kafka.js';
7+
import { type KafkaRecord } from '../types/schema.js';
8+
9+
/**
10+
* Kafka event envelope to extract data within body key
11+
* The record's body parameter is a string, though it can also be a JSON encoded string.
12+
* Regardless of its type it'll be parsed into a BaseModel object.
13+
*
14+
* Note: Records will be parsed the same way so if model is str,
15+
* all items in the list will be parsed as str and not as JSON (and vice versa)
16+
*/
17+
export const kafkaEnvelope = <T extends ZodSchema>(
18+
data: unknown,
19+
schema: T
20+
): z.infer<T> => {
21+
// manually fetch event source to deside between Msk or SelfManaged
22+
23+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
24+
// @ts-ignore
25+
const eventSource = data['eventSource'];
26+
27+
const parsedEnvelope:
28+
| z.infer<typeof KafkaMskEventSchema>
29+
| z.infer<typeof KafkaSelfManagedEventSchema> =
30+
eventSource === 'aws:kafka'
31+
? KafkaMskEventSchema.parse(data)
32+
: KafkaSelfManagedEventSchema.parse(data);
33+
34+
return Object.values(parsedEnvelope.records).map((topicRecord) => {
35+
return topicRecord.map((record: KafkaRecord) => {
36+
return parse(record.value, schema);
37+
});
38+
});
39+
};

Diff for: packages/parser/src/envelopes/kinesis-firehose.ts

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { parse } from './envelope.js';
2+
import { z, ZodSchema } from 'zod';
3+
import { KinesisFirehoseSchema } from '../schemas/kinesis-firehose.js';
4+
5+
/**
6+
* Kinesis Firehose Envelope to extract array of Records
7+
*
8+
* The record's data parameter is a base64 encoded string which is parsed into a bytes array,
9+
* though it can also be a JSON encoded string.
10+
* Regardless of its type it'll be parsed into a BaseModel object.
11+
*
12+
* Note: Records will be parsed the same way so if model is str,
13+
* all items in the list will be parsed as str and not as JSON (and vice versa)
14+
*
15+
* https://docs.aws.amazon.com/lambda/latest/dg/services-kinesisfirehose.html
16+
*/
17+
export const kinesisFirehoseEnvelope = <T extends ZodSchema>(
18+
data: unknown,
19+
schema: T
20+
): z.infer<T> => {
21+
const parsedEnvelope = KinesisFirehoseSchema.parse(data);
22+
23+
return parsedEnvelope.records.map((record) => {
24+
return parse(record.data, schema);
25+
});
26+
};

Diff for: packages/parser/src/envelopes/kinesis.ts

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { parse } from './envelope.js';
2+
import { z, ZodSchema } from 'zod';
3+
import { KinesisDataStreamSchema } from '../schemas/kinesis.js';
4+
5+
/**
6+
* Kinesis Data Stream Envelope to extract array of Records
7+
*
8+
* The record's data parameter is a base64 encoded string which is parsed into a bytes array,
9+
* though it can also be a JSON encoded string.
10+
* Regardless of its type it'll be parsed into a BaseModel object.
11+
*
12+
* Note: Records will be parsed the same way so if model is str,
13+
* all items in the list will be parsed as str and not as JSON (and vice versa)
14+
*/
15+
export const kinesisEnvelope = <T extends ZodSchema>(
16+
data: unknown,
17+
schema: T
18+
): z.infer<T> => {
19+
const parsedEnvelope = KinesisDataStreamSchema.parse(data);
20+
21+
return parsedEnvelope.Records.map((record) => {
22+
return parse(record.kinesis.data, schema);
23+
});
24+
};

Diff for: packages/parser/src/envelopes/lambda.ts

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { parse } from './envelope.js';
2+
import { z, ZodSchema } from 'zod';
3+
import { LambdaFunctionUrlSchema } from '../schemas/lambda.js';
4+
5+
/**
6+
* Lambda function URL envelope to extract data within body key
7+
*/
8+
export const lambdaFunctionUrlEnvelope = <T extends ZodSchema>(
9+
data: unknown,
10+
schema: T
11+
): z.infer<T> => {
12+
const parsedEnvelope = LambdaFunctionUrlSchema.parse(data);
13+
if (!parsedEnvelope.body) {
14+
throw new Error('Body field of Lambda function URL event is undefined');
15+
}
16+
17+
return parse(parsedEnvelope.body, schema);
18+
};

0 commit comments

Comments
 (0)