Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(parser): implement safeParse option #2244

Merged
merged 18 commits into from
Mar 21, 2024
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 33 additions & 11 deletions packages/parser/src/envelopes/apigw.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,40 @@
import { parse } from './envelope.js';
import { z, ZodSchema } from 'zod';
import { Envelope } from './envelope.js';
dreamorosi marked this conversation as resolved.
Show resolved Hide resolved
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 = <T extends ZodSchema>(
data: unknown,
schema: T
): z.infer<T> => {
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<T extends ZodSchema>(
data: unknown,
schema: T
): z.infer<T> {
return super.parse(APIGatewayProxyEventSchema.parse(data).body, schema);
}

return parse(parsedEnvelope.body, schema);
};
public static safeParse<T extends ZodSchema>(
data: unknown,
schema: T
): ParsedResult<unknown, z.infer<T>> {
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;
}
}
44 changes: 33 additions & 11 deletions packages/parser/src/envelopes/apigwv2.ts
Original file line number Diff line number Diff line change
@@ -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 = <T extends ZodSchema>(
data: unknown,
schema: T
): z.infer<T> => {
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<T extends ZodSchema>(
data: unknown,
schema: T
): z.infer<T> {
return super.parse(APIGatewayProxyEventV2Schema.parse(data).body, schema);
}

return parse(parsedEnvelope.body, schema);
};
public static safeParse<T extends ZodSchema>(
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;
}
}
61 changes: 49 additions & 12 deletions packages/parser/src/envelopes/cloudwatch.ts
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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 = <T extends ZodSchema>(
data: unknown,
schema: T
): z.infer<T> => {
const parsedEnvelope = CloudWatchLogsSchema.parse(data);
export class CloudWatchEnvelope extends Envelope {
public static parse<T extends ZodSchema>(
data: unknown,
schema: T
): z.infer<T> {
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<T extends ZodSchema>(
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<T>[] = [];

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,
};
}
}
69 changes: 56 additions & 13 deletions packages/parser/src/envelopes/dynamodb.ts
Original file line number Diff line number Diff line change
@@ -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<T extends ZodSchema> = {
NewImage: z.infer<T>;
Expand All @@ -13,16 +14,58 @@ type DynamoDBStreamEnvelopeResponse<T extends ZodSchema> = {
* 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 = <T extends ZodSchema>(
data: unknown,
schema: T
): DynamoDBStreamEnvelopeResponse<T>[] => {
const parsedEnvelope = DynamoDBStreamSchema.parse(data);
export class DynamoDBStreamEnvelope extends Envelope {
public static parse<T extends ZodSchema>(
data: unknown,
schema: T
): DynamoDBStreamEnvelopeResponse<z.infer<T>>[] {
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<T extends ZodSchema>(
data: unknown,
schema: T
): ParsedResult {
const parsedEnvelope = DynamoDBStreamSchema.safeParse(data);

if (!parsedEnvelope.success) {
return {
success: false,
error: parsedEnvelope.error,
originalEvent: data,
};
}
const parsedLogEvents: DynamoDBStreamEnvelopeResponse<z.infer<T>>[] = [];

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<unknown>).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,
};
});
};
}
}
91 changes: 69 additions & 22 deletions packages/parser/src/envelopes/envelope.ts
Original file line number Diff line number Diff line change
@@ -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 = <T extends ZodSchema>(
data: unknown,
schema: T
): z.infer<T>[] => {
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 = <T extends ZodSchema>(
data: unknown,
schema: T
): z.infer<T> => {
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 = <T extends ZodSchema>(
input: unknown,
schema: T
): ParsedResult<unknown, z.infer<T>> => {
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,
};
}
};
}
46 changes: 37 additions & 9 deletions packages/parser/src/envelopes/event-bridge.ts
Original file line number Diff line number Diff line change
@@ -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 = <T extends ZodSchema>(
data: unknown,
schema: T
): z.infer<T> => {
return parse(EventBridgeSchema.parse(data).detail, schema);
};
export class EventBridgeEnvelope extends Envelope {
public static parse<T extends ZodSchema>(
data: unknown,
schema: T
): z.infer<T> {
return super.parse(EventBridgeSchema.parse(data).detail, schema);
}

public static safeParse<T extends ZodSchema>(
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;
}
}
Loading