diff --git a/application/backend-credit-card-enrollment/backend-typescript/src/common/serializedEvent/Deserializer.ts b/application/backend-credit-card-enrollment/backend-typescript/src/common/serializedEvent/Deserializer.ts index c0f791a..9c0c080 100644 --- a/application/backend-credit-card-enrollment/backend-typescript/src/common/serializedEvent/Deserializer.ts +++ b/application/backend-credit-card-enrollment/backend-typescript/src/common/serializedEvent/Deserializer.ts @@ -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 { @@ -17,7 +16,7 @@ export class Deserializer { switch (serializedEvent.event_name) { case 'CreditCard_Enrollment_EnrollmentRequested': - return typeSafeCoercion(new EnrollmentRequested( + return new EnrollmentRequested( this.parseString(serializedEvent.event_id), this.parseString(serializedEvent.aggregate_id), this.parseNumber(serializedEvent.aggregate_version), @@ -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(new EnrollmentAccepted( + return new EnrollmentAccepted( this.parseString(serializedEvent.event_id), this.parseString(serializedEvent.aggregate_id), this.parseNumber(serializedEvent.aggregate_version), @@ -39,10 +38,10 @@ export class Deserializer { recordedOn, this.parseString(payload.reasonCode), this.parseString(payload.reasonDescription) - )); + ); case 'CreditCard_Enrollment_EnrollmentDeclined': - return typeSafeCoercion(new EnrollmentDeclined( + return new EnrollmentDeclined( this.parseString(serializedEvent.event_id), this.parseString(serializedEvent.aggregate_id), this.parseNumber(serializedEvent.aggregate_version), @@ -51,30 +50,30 @@ export class Deserializer { recordedOn, this.parseString(payload.reasonCode), this.parseString(payload.reasonDescription) - )); + ); case 'CreditCard_Product_ProductActivated': - return typeSafeCoercion(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(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(new ProductDefined( + return new ProductDefined( this.parseString(serializedEvent.event_id), this.parseString(serializedEvent.aggregate_id), this.parseNumber(serializedEvent.aggregate_version), @@ -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}`); diff --git a/application/backend-credit-card-enrollment/backend-typescript/src/common/util/ParseWithValidation.ts b/application/backend-credit-card-enrollment/backend-typescript/src/common/util/ParseWithValidation.ts new file mode 100644 index 0000000..ef8074d --- /dev/null +++ b/application/backend-credit-card-enrollment/backend-typescript/src/common/util/ParseWithValidation.ts @@ -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(data: unknown, schema?: z.ZodType): 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; + } +} \ No newline at end of file diff --git a/application/backend-credit-card-enrollment/backend-typescript/src/common/util/TypeSafeCoercion.ts b/application/backend-credit-card-enrollment/backend-typescript/src/common/util/TypeSafeCoercion.ts deleted file mode 100644 index 670acda..0000000 --- a/application/backend-credit-card-enrollment/backend-typescript/src/common/util/TypeSafeCoercion.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { z } from 'zod'; - -export function typeSafeCoercion(data: unknown): T { - const schema = z.custom().transform((val) => val as T); - return schema.parse(data); -} \ No newline at end of file diff --git a/application/backend-credit-card-enrollment/backend-typescript/src/creditCard/enrollment/command/EnrollmentCommandController.ts b/application/backend-credit-card-enrollment/backend-typescript/src/creditCard/enrollment/command/EnrollmentCommandController.ts index 9efb260..8f2b3f8 100644 --- a/application/backend-credit-card-enrollment/backend-typescript/src/creditCard/enrollment/command/EnrollmentCommandController.ts +++ b/application/backend-credit-card-enrollment/backend-typescript/src/creditCard/enrollment/command/EnrollmentCommandController.ts @@ -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 { @@ -32,7 +32,7 @@ export class EnrollmentCommandController extends CommandController { return; } - const requestBody = typeSafeCoercion(req.body); + const requestBody = parseWithValidation(req.body, requestEnrollmentHttpRequestSchema); const command = new RequestEnrollmentCommand( sessionToken, requestBody.productId, diff --git a/application/backend-credit-card-enrollment/backend-typescript/src/creditCard/enrollment/command/RequestEnrollmentHttpRequest.ts b/application/backend-credit-card-enrollment/backend-typescript/src/creditCard/enrollment/command/RequestEnrollmentHttpRequest.ts index b40693c..7a811f0 100644 --- a/application/backend-credit-card-enrollment/backend-typescript/src/creditCard/enrollment/command/RequestEnrollmentHttpRequest.ts +++ b/application/backend-credit-card-enrollment/backend-typescript/src/creditCard/enrollment/command/RequestEnrollmentHttpRequest.ts @@ -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; \ No newline at end of file