Skip to content

Commit

Permalink
Type safe coercion for typescript
Browse files Browse the repository at this point in the history
  • Loading branch information
galeaspablo committed Jan 6, 2025
1 parent 1862d0c commit 445ddf5
Show file tree
Hide file tree
Showing 5 changed files with 101 additions and 67 deletions.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@
"pg": "^8.8.0",
"reflect-metadata": "^0.2.2",
"tsyringe": "^4.8.0",
"winston": "^3.8.2"
"winston": "^3.8.2",
"zod": "^3.24.1"
},
"devDependencies": {
"@types/express": "^4.17.21",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { Event } from '../event/Event';
import { SerializedEvent } from './SerializedEvent';
import {EnrollmentRequested} from "../../creditCard/enrollment/event/EnrollmentRequested";
import {EnrollmentAccepted} from "../../creditCard/enrollment/event/EnrollmentAccepted";
import {EnrollmentDeclined} from "../../creditCard/enrollment/event/EnrollmentDeclined";
import {ProductActivated} from "../../creditCard/product/event/ProductActivated";
import {ProductDeactivated} from "../../creditCard/product/event/ProductDeactivated";
import {ProductDefined} from "../../creditCard/product/event/ProductDefined";
import {injectable} from "tsyringe";
import { EnrollmentRequested } from "../../creditCard/enrollment/event/EnrollmentRequested";
import { EnrollmentAccepted } from "../../creditCard/enrollment/event/EnrollmentAccepted";
import { EnrollmentDeclined } from "../../creditCard/enrollment/event/EnrollmentDeclined";
import { ProductActivated } from "../../creditCard/product/event/ProductActivated";
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 @@ -16,79 +17,79 @@ export class Deserializer {

switch (serializedEvent.event_name) {
case 'CreditCard_Enrollment_EnrollmentRequested':
return new EnrollmentRequested(
serializedEvent.event_id,
serializedEvent.aggregate_id,
serializedEvent.aggregate_version,
serializedEvent.correlation_id,
serializedEvent.causation_id,
return typeSafeCoercion<EnrollmentRequested>(new EnrollmentRequested(
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,
payload.userId,
payload.productId,
payload.annualIncomeInCents
);
this.parseString(payload.userId),
this.parseString(payload.productId),
this.parseNumber(payload.annualIncomeInCents)
));

case 'CreditCard_Enrollment_EnrollmentAccepted':
return new EnrollmentAccepted(
serializedEvent.event_id,
serializedEvent.aggregate_id,
serializedEvent.aggregate_version,
serializedEvent.correlation_id,
serializedEvent.causation_id,
return typeSafeCoercion<EnrollmentAccepted>(new EnrollmentAccepted(
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,
payload.reasonCode,
payload.reasonDescription
);
this.parseString(payload.reasonCode),
this.parseString(payload.reasonDescription)
));

case 'CreditCard_Enrollment_EnrollmentDeclined':
return new EnrollmentDeclined(
serializedEvent.event_id,
serializedEvent.aggregate_id,
serializedEvent.aggregate_version,
serializedEvent.correlation_id,
serializedEvent.causation_id,
return typeSafeCoercion<EnrollmentDeclined>(new EnrollmentDeclined(
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,
payload.reasonCode,
payload.reasonDescription
);
this.parseString(payload.reasonCode),
this.parseString(payload.reasonDescription)
));

case 'CreditCard_Product_ProductActivated':
return new ProductActivated(
serializedEvent.event_id,
serializedEvent.aggregate_id,
serializedEvent.aggregate_version,
serializedEvent.correlation_id,
serializedEvent.causation_id,
return typeSafeCoercion<ProductActivated>(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 new ProductDeactivated(
serializedEvent.event_id,
serializedEvent.aggregate_id,
serializedEvent.aggregate_version,
serializedEvent.correlation_id,
serializedEvent.causation_id,
return typeSafeCoercion<ProductDeactivated>(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 new ProductDefined(
serializedEvent.event_id,
serializedEvent.aggregate_id,
serializedEvent.aggregate_version,
serializedEvent.correlation_id,
serializedEvent.causation_id,
return typeSafeCoercion<ProductDefined>(new ProductDefined(
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,
payload.name,
payload.interestInBasisPoints,
payload.annualFeeInCents,
payload.paymentCycle,
payload.creditLimitInCents,
payload.maxBalanceTransferAllowedInCents,
payload.reward,
payload.cardBackgroundHex
);
this.parseString(payload.name),
this.parseNumber(payload.interestInBasisPoints),
this.parseNumber(payload.annualFeeInCents),
this.parseString(payload.paymentCycle),
this.parseNumber(payload.creditLimitInCents),
this.parseNumber(payload.maxBalanceTransferAllowedInCents),
this.parseString(payload.reward),
this.parseString(payload.cardBackgroundHex)
));

default:
throw new Error(`Unknown event type: ${serializedEvent.event_name}`);
Expand All @@ -105,4 +106,19 @@ export class Deserializer {
}
return parsed;
}

private parseString(value: any): string {
if (typeof value !== 'string') {
throw new Error(`Expected string but got ${typeof value}`);
}
return value;
}

private parseNumber(value: any): number {
const parsed = Number(value);
if (isNaN(parsed)) {
throw new Error(`Expected number but got ${typeof value}`);
}
return parsed;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { z } from 'zod';

export function typeSafeCoercion<T>(data: unknown): T {
const schema = z.custom<T>().transform((val) => val as T);
return schema.parse(data);
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { RequestEnrollmentCommandHandler } from './RequestEnrollmentCommandHandl
import { RequestEnrollmentCommand } from './RequestEnrollmentCommand';
import { RequestEnrollmentHttpRequest } from './RequestEnrollmentHttpRequest';
import {inject, injectable} from "tsyringe";
import {typeSafeCoercion} from "../../../common/util/TypeSafeCoercion";

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

const requestBody: RequestEnrollmentHttpRequest = req.body;
const requestBody = typeSafeCoercion<RequestEnrollmentHttpRequest>(req.body);
const command = new RequestEnrollmentCommand(
sessionToken,
requestBody.productId,
Expand Down

0 comments on commit 445ddf5

Please sign in to comment.