Skip to content

Commit 445ddf5

Browse files
committed
Type safe coercion for typescript
1 parent 1862d0c commit 445ddf5

File tree

5 files changed

+101
-67
lines changed

5 files changed

+101
-67
lines changed

application/backend-credit-card-enrollment/backend-typescript/package-lock.json

Lines changed: 11 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

application/backend-credit-card-enrollment/backend-typescript/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@
1212
"pg": "^8.8.0",
1313
"reflect-metadata": "^0.2.2",
1414
"tsyringe": "^4.8.0",
15-
"winston": "^3.8.2"
15+
"winston": "^3.8.2",
16+
"zod": "^3.24.1"
1617
},
1718
"devDependencies": {
1819
"@types/express": "^4.17.21",
Lines changed: 80 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
import { Event } from '../event/Event';
22
import { SerializedEvent } from './SerializedEvent';
3-
import {EnrollmentRequested} from "../../creditCard/enrollment/event/EnrollmentRequested";
4-
import {EnrollmentAccepted} from "../../creditCard/enrollment/event/EnrollmentAccepted";
5-
import {EnrollmentDeclined} from "../../creditCard/enrollment/event/EnrollmentDeclined";
6-
import {ProductActivated} from "../../creditCard/product/event/ProductActivated";
7-
import {ProductDeactivated} from "../../creditCard/product/event/ProductDeactivated";
8-
import {ProductDefined} from "../../creditCard/product/event/ProductDefined";
9-
import {injectable} from "tsyringe";
3+
import { EnrollmentRequested } from "../../creditCard/enrollment/event/EnrollmentRequested";
4+
import { EnrollmentAccepted } from "../../creditCard/enrollment/event/EnrollmentAccepted";
5+
import { EnrollmentDeclined } from "../../creditCard/enrollment/event/EnrollmentDeclined";
6+
import { ProductActivated } from "../../creditCard/product/event/ProductActivated";
7+
import { ProductDeactivated } from "../../creditCard/product/event/ProductDeactivated";
8+
import { ProductDefined } from "../../creditCard/product/event/ProductDefined";
9+
import { injectable } from "tsyringe";
10+
import { typeSafeCoercion } from "../util/TypeSafeCoercion";
1011

1112
@injectable()
1213
export class Deserializer {
@@ -16,79 +17,79 @@ export class Deserializer {
1617

1718
switch (serializedEvent.event_name) {
1819
case 'CreditCard_Enrollment_EnrollmentRequested':
19-
return new EnrollmentRequested(
20-
serializedEvent.event_id,
21-
serializedEvent.aggregate_id,
22-
serializedEvent.aggregate_version,
23-
serializedEvent.correlation_id,
24-
serializedEvent.causation_id,
20+
return typeSafeCoercion<EnrollmentRequested>(new EnrollmentRequested(
21+
this.parseString(serializedEvent.event_id),
22+
this.parseString(serializedEvent.aggregate_id),
23+
this.parseNumber(serializedEvent.aggregate_version),
24+
this.parseString(serializedEvent.correlation_id),
25+
this.parseString(serializedEvent.causation_id),
2526
recordedOn,
26-
payload.userId,
27-
payload.productId,
28-
payload.annualIncomeInCents
29-
);
27+
this.parseString(payload.userId),
28+
this.parseString(payload.productId),
29+
this.parseNumber(payload.annualIncomeInCents)
30+
));
3031

3132
case 'CreditCard_Enrollment_EnrollmentAccepted':
32-
return new EnrollmentAccepted(
33-
serializedEvent.event_id,
34-
serializedEvent.aggregate_id,
35-
serializedEvent.aggregate_version,
36-
serializedEvent.correlation_id,
37-
serializedEvent.causation_id,
33+
return typeSafeCoercion<EnrollmentAccepted>(new EnrollmentAccepted(
34+
this.parseString(serializedEvent.event_id),
35+
this.parseString(serializedEvent.aggregate_id),
36+
this.parseNumber(serializedEvent.aggregate_version),
37+
this.parseString(serializedEvent.correlation_id),
38+
this.parseString(serializedEvent.causation_id),
3839
recordedOn,
39-
payload.reasonCode,
40-
payload.reasonDescription
41-
);
40+
this.parseString(payload.reasonCode),
41+
this.parseString(payload.reasonDescription)
42+
));
4243

4344
case 'CreditCard_Enrollment_EnrollmentDeclined':
44-
return new EnrollmentDeclined(
45-
serializedEvent.event_id,
46-
serializedEvent.aggregate_id,
47-
serializedEvent.aggregate_version,
48-
serializedEvent.correlation_id,
49-
serializedEvent.causation_id,
45+
return typeSafeCoercion<EnrollmentDeclined>(new EnrollmentDeclined(
46+
this.parseString(serializedEvent.event_id),
47+
this.parseString(serializedEvent.aggregate_id),
48+
this.parseNumber(serializedEvent.aggregate_version),
49+
this.parseString(serializedEvent.correlation_id),
50+
this.parseString(serializedEvent.causation_id),
5051
recordedOn,
51-
payload.reasonCode,
52-
payload.reasonDescription
53-
);
52+
this.parseString(payload.reasonCode),
53+
this.parseString(payload.reasonDescription)
54+
));
5455

5556
case 'CreditCard_Product_ProductActivated':
56-
return new ProductActivated(
57-
serializedEvent.event_id,
58-
serializedEvent.aggregate_id,
59-
serializedEvent.aggregate_version,
60-
serializedEvent.correlation_id,
61-
serializedEvent.causation_id,
57+
return typeSafeCoercion<ProductActivated>(new ProductActivated(
58+
this.parseString(serializedEvent.event_id),
59+
this.parseString(serializedEvent.aggregate_id),
60+
this.parseNumber(serializedEvent.aggregate_version),
61+
this.parseString(serializedEvent.correlation_id),
62+
this.parseString(serializedEvent.causation_id),
6263
recordedOn
63-
);
64+
));
6465

6566
case 'CreditCard_Product_ProductDeactivated':
66-
return new ProductDeactivated(
67-
serializedEvent.event_id,
68-
serializedEvent.aggregate_id,
69-
serializedEvent.aggregate_version,
70-
serializedEvent.correlation_id,
71-
serializedEvent.causation_id,
67+
return typeSafeCoercion<ProductDeactivated>(new ProductDeactivated(
68+
this.parseString(serializedEvent.event_id),
69+
this.parseString(serializedEvent.aggregate_id),
70+
this.parseNumber(serializedEvent.aggregate_version),
71+
this.parseString(serializedEvent.correlation_id),
72+
this.parseString(serializedEvent.causation_id),
7273
recordedOn
73-
);
74+
));
7475

7576
case 'CreditCard_Product_ProductDefined':
76-
return new ProductDefined(
77-
serializedEvent.event_id,
78-
serializedEvent.aggregate_id,
79-
serializedEvent.aggregate_version,
80-
serializedEvent.correlation_id,
81-
serializedEvent.causation_id,
77+
return typeSafeCoercion<ProductDefined>(new ProductDefined(
78+
this.parseString(serializedEvent.event_id),
79+
this.parseString(serializedEvent.aggregate_id),
80+
this.parseNumber(serializedEvent.aggregate_version),
81+
this.parseString(serializedEvent.correlation_id),
82+
this.parseString(serializedEvent.causation_id),
8283
recordedOn,
83-
payload.name,
84-
payload.interestInBasisPoints,
85-
payload.annualFeeInCents,
86-
payload.paymentCycle,
87-
payload.creditLimitInCents,
88-
payload.maxBalanceTransferAllowedInCents,
89-
payload.reward,
90-
payload.cardBackgroundHex
91-
);
84+
this.parseString(payload.name),
85+
this.parseNumber(payload.interestInBasisPoints),
86+
this.parseNumber(payload.annualFeeInCents),
87+
this.parseString(payload.paymentCycle),
88+
this.parseNumber(payload.creditLimitInCents),
89+
this.parseNumber(payload.maxBalanceTransferAllowedInCents),
90+
this.parseString(payload.reward),
91+
this.parseString(payload.cardBackgroundHex)
92+
));
9293

9394
default:
9495
throw new Error(`Unknown event type: ${serializedEvent.event_name}`);
@@ -105,4 +106,19 @@ export class Deserializer {
105106
}
106107
return parsed;
107108
}
109+
110+
private parseString(value: any): string {
111+
if (typeof value !== 'string') {
112+
throw new Error(`Expected string but got ${typeof value}`);
113+
}
114+
return value;
115+
}
116+
117+
private parseNumber(value: any): number {
118+
const parsed = Number(value);
119+
if (isNaN(parsed)) {
120+
throw new Error(`Expected number but got ${typeof value}`);
121+
}
122+
return parsed;
123+
}
108124
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { z } from 'zod';
2+
3+
export function typeSafeCoercion<T>(data: unknown): T {
4+
const schema = z.custom<T>().transform((val) => val as T);
5+
return schema.parse(data);
6+
}

application/backend-credit-card-enrollment/backend-typescript/src/creditCard/enrollment/command/EnrollmentCommandController.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { RequestEnrollmentCommandHandler } from './RequestEnrollmentCommandHandl
66
import { RequestEnrollmentCommand } from './RequestEnrollmentCommand';
77
import { RequestEnrollmentHttpRequest } from './RequestEnrollmentHttpRequest';
88
import {inject, injectable} from "tsyringe";
9+
import {typeSafeCoercion} from "../../../common/util/TypeSafeCoercion";
910

1011
@injectable()
1112
export class EnrollmentCommandController extends CommandController {
@@ -31,7 +32,7 @@ export class EnrollmentCommandController extends CommandController {
3132
return;
3233
}
3334

34-
const requestBody: RequestEnrollmentHttpRequest = req.body;
35+
const requestBody = typeSafeCoercion<RequestEnrollmentHttpRequest>(req.body);
3536
const command = new RequestEnrollmentCommand(
3637
sessionToken,
3738
requestBody.productId,

0 commit comments

Comments
 (0)