Skip to content

Commit

Permalink
Do request validation properly with zod
Browse files Browse the repository at this point in the history
  • Loading branch information
galeaspablo committed Jan 7, 2025
1 parent 578e1a0 commit 4cc093d
Show file tree
Hide file tree
Showing 5 changed files with 48 additions and 26 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import { ProductActivated } from "../../creditCard/product/event/ProductActivate
import { ProductDeactivated } from "../../creditCard/product/event/ProductDeactivated";
import { ProductDefined } from "../../creditCard/product/event/ProductDefined";
import { injectable } from "tsyringe";
import { typeSafeCoercion } from "../util/TypeSafeCoercion";

@injectable()
export class Deserializer {
Expand All @@ -17,7 +16,7 @@ export class Deserializer {

switch (serializedEvent.event_name) {
case 'CreditCard_Enrollment_EnrollmentRequested':
return typeSafeCoercion<EnrollmentRequested>(new EnrollmentRequested(
return new EnrollmentRequested(
this.parseString(serializedEvent.event_id),
this.parseString(serializedEvent.aggregate_id),
this.parseNumber(serializedEvent.aggregate_version),
Expand All @@ -27,10 +26,10 @@ export class Deserializer {
this.parseString(payload.userId),
this.parseString(payload.productId),
this.parseNumber(payload.annualIncomeInCents)
));
);

case 'CreditCard_Enrollment_EnrollmentAccepted':
return typeSafeCoercion<EnrollmentAccepted>(new EnrollmentAccepted(
return new EnrollmentAccepted(
this.parseString(serializedEvent.event_id),
this.parseString(serializedEvent.aggregate_id),
this.parseNumber(serializedEvent.aggregate_version),
Expand All @@ -39,10 +38,10 @@ export class Deserializer {
recordedOn,
this.parseString(payload.reasonCode),
this.parseString(payload.reasonDescription)
));
);

case 'CreditCard_Enrollment_EnrollmentDeclined':
return typeSafeCoercion<EnrollmentDeclined>(new EnrollmentDeclined(
return new EnrollmentDeclined(
this.parseString(serializedEvent.event_id),
this.parseString(serializedEvent.aggregate_id),
this.parseNumber(serializedEvent.aggregate_version),
Expand All @@ -51,30 +50,30 @@ export class Deserializer {
recordedOn,
this.parseString(payload.reasonCode),
this.parseString(payload.reasonDescription)
));
);

case 'CreditCard_Product_ProductActivated':
return typeSafeCoercion<ProductActivated>(new ProductActivated(
return new ProductActivated(
this.parseString(serializedEvent.event_id),
this.parseString(serializedEvent.aggregate_id),
this.parseNumber(serializedEvent.aggregate_version),
this.parseString(serializedEvent.correlation_id),
this.parseString(serializedEvent.causation_id),
recordedOn
));
);

case 'CreditCard_Product_ProductDeactivated':
return typeSafeCoercion<ProductDeactivated>(new ProductDeactivated(
return new ProductDeactivated(
this.parseString(serializedEvent.event_id),
this.parseString(serializedEvent.aggregate_id),
this.parseNumber(serializedEvent.aggregate_version),
this.parseString(serializedEvent.correlation_id),
this.parseString(serializedEvent.causation_id),
recordedOn
));
);

case 'CreditCard_Product_ProductDefined':
return typeSafeCoercion<ProductDefined>(new ProductDefined(
return new ProductDefined(
this.parseString(serializedEvent.event_id),
this.parseString(serializedEvent.aggregate_id),
this.parseNumber(serializedEvent.aggregate_version),
Expand All @@ -89,7 +88,7 @@ export class Deserializer {
this.parseNumber(payload.maxBalanceTransferAllowedInCents),
this.parseString(payload.reward),
this.parseString(payload.cardBackgroundHex)
));
);

default:
throw new Error(`Unknown event type: ${serializedEvent.event_name}`);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import {z} from 'zod';

export class ValidationError extends Error {
constructor(
public readonly errors: z.ZodError
) {
super('Validation failed');
this.name = 'ValidationError';
}
}

export function parseWithValidation<T>(data: unknown, schema?: z.ZodType<T>): T {
if (!schema) {
throw new Error('Schema must be provided');
}

try {
return schema.parse(data);
} catch (error) {
if (error instanceof z.ZodError) {
throw new ValidationError(error);
}
throw error;
}
}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ import { PostgresTransactionalEventStore } from '../../../common/eventStore/Post
import { MongoTransactionalProjectionOperator } from '../../../common/projection/MongoTransactionalProjectionOperator';
import { RequestEnrollmentCommandHandler } from './RequestEnrollmentCommandHandler';
import { RequestEnrollmentCommand } from './RequestEnrollmentCommand';
import { RequestEnrollmentHttpRequest } from './RequestEnrollmentHttpRequest';
import {requestEnrollmentHttpRequestSchema} from './RequestEnrollmentHttpRequest';
import {inject, injectable} from "tsyringe";
import {typeSafeCoercion} from "../../../common/util/TypeSafeCoercion";
import {parseWithValidation} from "../../../common/util/ParseWithValidation";

@injectable()
export class EnrollmentCommandController extends CommandController {
Expand All @@ -32,7 +32,7 @@ export class EnrollmentCommandController extends CommandController {
return;
}

const requestBody = typeSafeCoercion<RequestEnrollmentHttpRequest>(req.body);
const requestBody = parseWithValidation(req.body, requestEnrollmentHttpRequestSchema);
const command = new RequestEnrollmentCommand(
sessionToken,
requestBody.productId,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
export interface RequestEnrollmentHttpRequest {
productId: string;
annualIncomeInCents: number;
}
import { z } from 'zod';

export const requestEnrollmentHttpRequestSchema = z.object({
productId: z.string(),
annualIncomeInCents: z.number().min(0, "Annual income cannot be negative").max(1_000_000_000, "Annual income is too high")
});

export type RequestEnrollmentHttpRequest = z.infer<typeof requestEnrollmentHttpRequestSchema>;

0 comments on commit 4cc093d

Please sign in to comment.