Skip to content

Commit 63191dd

Browse files
authored
Merge pull request #51 from josecarlospeer-cloud/feat/write-repository-exists
2 parents 33e0c19 + 53367ed commit 63191dd

9 files changed

Lines changed: 141 additions & 56 deletions

File tree

Lines changed: 9 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,24 @@
1-
import { Application, EventSourcing } from "hollywood-js";
1+
import { Application } from "hollywood-js";
22
import type { IAppError } from "hollywood-js/src/Application/Bus/CallbackArg";
33
import { inject, injectable } from "inversify";
44
import NotFoundException from "../../../Shared/Domain/Exceptions/NotFoundException";
5-
import Transaction from "../../Domain/Transaction";
5+
import ITransactionWriteRepository from "../../Domain/WriteRepository";
66
import ConfirmCommand from "./Command";
77

88
@injectable()
99
export default class Confirm implements Application.ICommandHandler {
1010
constructor(
11-
@inject("infrastructure.transaction.eventStore")
12-
private readonly writeModel: EventSourcing.EventStore<Transaction>,
11+
@inject("infrastructure.transaction.writeRepository")
12+
private readonly writeModel: ITransactionWriteRepository,
1313
) {}
1414

1515
@Application.autowiring
1616
public async handle(command: ConfirmCommand): Promise<void | IAppError> {
17-
try {
18-
const transaction = await this.writeModel.load(command.uuid.toIdentity()) as Transaction;
19-
transaction.confirm();
20-
await this.writeModel.save(transaction);
21-
} catch (err) {
22-
if (err instanceof EventSourcing.AggregateRootNotFoundException) {
23-
throw new NotFoundException("Transaction not found");
24-
}
25-
throw err;
17+
if (!await this.writeModel.exists(command.uuid)) {
18+
throw new NotFoundException("Transaction not found");
2619
}
20+
const transaction = await this.writeModel.load(command.uuid);
21+
transaction.confirm();
22+
await this.writeModel.save(transaction);
2723
}
2824
}
Lines changed: 12 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import Probe from "@Shared/Infrastructure/Audit/Probe";
2-
import { Application, EventSourcing } from "hollywood-js";
2+
import { Application } from "hollywood-js";
33
import type { IAppError } from "hollywood-js/src/Application/Bus/CallbackArg";
44
import { inject, injectable } from "inversify";
55
import type { Counter } from "prom-client";
66
import ConflictException from "../../../Shared/Domain/Exceptions/ConflictException";
77
import Transaction from "../../Domain/Transaction";
8+
import ITransactionWriteRepository from "../../Domain/WriteRepository";
89
import CreateCommand from "./Command";
910

1011
@injectable()
@@ -14,9 +15,8 @@ export default class Create implements Application.ICommandHandler {
1415
private readonly success: Counter<string>;
1516

1617
constructor(
17-
@inject(
18-
"infrastructure.transaction.eventStore",
19-
) private readonly writeModel: EventSourcing.EventStore<Transaction>,
18+
@inject("infrastructure.transaction.writeRepository")
19+
private readonly writeModel: ITransactionWriteRepository,
2020
) {
2121
this.error = Probe.counter({ name: "transaction_create_error", help: "Counter of the incremental transaction create errors"});
2222
this.conflicts = Probe.counter({ name: "transaction_create_conflict", help: "Counter of the incremental transaction create conflicts"});
@@ -25,19 +25,9 @@ export default class Create implements Application.ICommandHandler {
2525

2626
@Application.autowiring
2727
public async handle(command: CreateCommand): Promise<void | IAppError> {
28-
29-
try {
30-
await this.writeModel.load(command.uuid.toIdentity());
28+
if (await this.writeModel.exists(command.uuid)) {
3129
this.conflicts.inc(1);
3230
throw new ConflictException("Already exists");
33-
} catch (err) {
34-
if (err instanceof ConflictException) {
35-
throw err;
36-
}
37-
if (!(err instanceof EventSourcing.AggregateRootNotFoundException)) {
38-
this.error.inc(1);
39-
throw err;
40-
}
4131
}
4232

4333
const transaction: Transaction = Transaction.create(
@@ -46,7 +36,12 @@ export default class Create implements Application.ICommandHandler {
4636
command.price,
4737
);
4838

49-
await this.writeModel.save(transaction);
50-
this.success.inc(1);
39+
try {
40+
await this.writeModel.save(transaction);
41+
this.success.inc(1);
42+
} catch (err) {
43+
this.error.inc(1);
44+
throw err;
45+
}
5146
}
5247
}
Lines changed: 9 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,24 @@
1-
import { Application, EventSourcing } from "hollywood-js";
1+
import { Application } from "hollywood-js";
22
import type { IAppError } from "hollywood-js/src/Application/Bus/CallbackArg";
33
import { inject, injectable } from "inversify";
44
import NotFoundException from "../../../Shared/Domain/Exceptions/NotFoundException";
5-
import Transaction from "../../Domain/Transaction";
5+
import ITransactionWriteRepository from "../../Domain/WriteRepository";
66
import FailCommand from "./Command";
77

88
@injectable()
99
export default class Fail implements Application.ICommandHandler {
1010
constructor(
11-
@inject("infrastructure.transaction.eventStore")
12-
private readonly writeModel: EventSourcing.EventStore<Transaction>,
11+
@inject("infrastructure.transaction.writeRepository")
12+
private readonly writeModel: ITransactionWriteRepository,
1313
) {}
1414

1515
@Application.autowiring
1616
public async handle(command: FailCommand): Promise<void | IAppError> {
17-
try {
18-
const transaction = await this.writeModel.load(command.uuid.toIdentity()) as Transaction;
19-
transaction.fail(command.reason);
20-
await this.writeModel.save(transaction);
21-
} catch (err) {
22-
if (err instanceof EventSourcing.AggregateRootNotFoundException) {
23-
throw new NotFoundException("Transaction not found");
24-
}
25-
throw err;
17+
if (!await this.writeModel.exists(command.uuid)) {
18+
throw new NotFoundException("Transaction not found");
2619
}
20+
const transaction = await this.writeModel.load(command.uuid);
21+
transaction.fail(command.reason);
22+
await this.writeModel.save(transaction);
2723
}
2824
}
Lines changed: 9 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,24 @@
1-
import { Application, EventSourcing } from "hollywood-js";
1+
import { Application } from "hollywood-js";
22
import type { IAppError } from "hollywood-js/src/Application/Bus/CallbackArg";
33
import { inject, injectable } from "inversify";
44
import NotFoundException from "../../../Shared/Domain/Exceptions/NotFoundException";
5-
import Transaction from "../../Domain/Transaction";
5+
import ITransactionWriteRepository from "../../Domain/WriteRepository";
66
import RefundCommand from "./Command";
77

88
@injectable()
99
export default class Refund implements Application.ICommandHandler {
1010
constructor(
11-
@inject("infrastructure.transaction.eventStore")
12-
private readonly writeModel: EventSourcing.EventStore<Transaction>,
11+
@inject("infrastructure.transaction.writeRepository")
12+
private readonly writeModel: ITransactionWriteRepository,
1313
) {}
1414

1515
@Application.autowiring
1616
public async handle(command: RefundCommand): Promise<void | IAppError> {
17-
try {
18-
const transaction = await this.writeModel.load(command.uuid.toIdentity()) as Transaction;
19-
transaction.refund();
20-
await this.writeModel.save(transaction);
21-
} catch (err) {
22-
if (err instanceof EventSourcing.AggregateRootNotFoundException) {
23-
throw new NotFoundException("Transaction not found");
24-
}
25-
throw err;
17+
if (!await this.writeModel.exists(command.uuid)) {
18+
throw new NotFoundException("Transaction not found");
2619
}
20+
const transaction = await this.writeModel.load(command.uuid);
21+
transaction.refund();
22+
await this.writeModel.save(transaction);
2723
}
2824
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import type Transaction from "./Transaction";
2+
import type TransactionId from "./ValueObject/TransactionId";
3+
4+
/**
5+
* Write-side repository interface for the Transaction aggregate.
6+
*
7+
* Abstracts the EventStore so command handlers depend on a domain interface
8+
* instead of the infrastructure EventStore class directly, and to encapsulate
9+
* the existence check cleanly.
10+
*
11+
* Source: Vernon (IDDD), p. 356 — "Command handlers orchestrate; they do not
12+
* query to make decisions." Using exists() avoids catch-to-check anti-pattern.
13+
*/
14+
export default interface ITransactionWriteRepository {
15+
exists(id: TransactionId): Promise<boolean>;
16+
load(id: TransactionId): Promise<Transaction>;
17+
save(transaction: Transaction): Promise<void>;
18+
}

src/Billing/Transaction/Infrastructure/TransactionModule.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import Transaction from "@Transaction/Domain/Transaction";
1414
import {Transactions} from "@Transaction/Infrastructure/ReadModel/Mapping/Transactions";
1515
import PostgresProjector from "@Transaction/Infrastructure/ReadModel/Projections/PostgresProjector";
1616
import PostgresRepository from "@Transaction/Infrastructure/ReadModel/Repository/PostgresRepository";
17+
import EventStoreTransactionRepository from "@Transaction/Infrastructure/WriteModel/EventStoreTransactionRepository";
1718
import { EventSourcing, Framework} from "hollywood-js";
1819
import type {interfaces} from "inversify";
1920
import {getRepository} from "typeorm";
@@ -38,6 +39,7 @@ export const services = (new Map())
3839
],
3940
})
4041
.set("infrastructure.transaction.eventStore", { eventStore: Transaction })
42+
.set("infrastructure.transaction.writeRepository", { instance: EventStoreTransactionRepository })
4143
.set(Framework.SERVICES_ALIAS.COMMAND_MIDDLEWARE, { collection: [
4244
LoggerMiddleware,
4345
]})
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import Transaction from "@Transaction/Domain/Transaction";
2+
import TransactionId from "@Transaction/Domain/ValueObject/TransactionId";
3+
import ITransactionWriteRepository from "@Transaction/Domain/WriteRepository";
4+
import { EventSourcing } from "hollywood-js";
5+
import { inject, injectable } from "inversify";
6+
7+
/**
8+
* Write-side repository implementation backed by the EventStore.
9+
*
10+
* Encapsulates the catch-to-check existence pattern in one place so that
11+
* command handlers can call exists() without relying on exception control flow.
12+
*/
13+
@injectable()
14+
export default class EventStoreTransactionRepository implements ITransactionWriteRepository {
15+
constructor(
16+
@inject("infrastructure.transaction.eventStore")
17+
private readonly eventStore: EventSourcing.EventStore<Transaction>,
18+
) {}
19+
20+
public async exists(id: TransactionId): Promise<boolean> {
21+
try {
22+
await this.eventStore.load(id.toIdentity());
23+
return true;
24+
} catch (err) {
25+
if (err instanceof EventSourcing.AggregateRootNotFoundException) {
26+
return false;
27+
}
28+
throw err;
29+
}
30+
}
31+
32+
public async load(id: TransactionId): Promise<Transaction> {
33+
return await this.eventStore.load(id.toIdentity()) as Transaction;
34+
}
35+
36+
public async save(transaction: Transaction): Promise<void> {
37+
await this.eventStore.save(transaction);
38+
}
39+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { EventSourcing } from "hollywood-js";
2+
import { inject, injectable } from "inversify";
3+
import Transaction from "@Transaction/Domain/Transaction";
4+
import ITransactionWriteRepository from "@Transaction/Domain/WriteRepository";
5+
import TransactionId from "@Transaction/Domain/ValueObject/TransactionId";
6+
7+
/**
8+
* In-memory ITransactionWriteRepository for tests.
9+
* Shares the same InMemoryEventStore as InMemoryTransactionRepository
10+
* so both the write-side (handlers) and the test assertions see the same state.
11+
*/
12+
@injectable()
13+
export class InMemoryWriteRepository implements ITransactionWriteRepository {
14+
constructor(
15+
@inject("infrastructure.transaction.eventStore")
16+
private readonly eventStore: EventSourcing.EventStore<Transaction>,
17+
) {}
18+
19+
public async exists(id: TransactionId): Promise<boolean> {
20+
try {
21+
await this.eventStore.load(id.toIdentity());
22+
return true;
23+
} catch (err) {
24+
if (err instanceof EventSourcing.AggregateRootNotFoundException) {
25+
return false;
26+
}
27+
throw err;
28+
}
29+
}
30+
31+
public async load(id: TransactionId): Promise<Transaction> {
32+
return await this.eventStore.load(id.toIdentity()) as Transaction;
33+
}
34+
35+
public async save(transaction: Transaction): Promise<void> {
36+
await this.eventStore.save(transaction);
37+
}
38+
}

tests/TestKernelFactory.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import TransactionWasCreated from "@Transaction/Domain/Events/TransactionWasCrea
88
import TransactionWasRefunded from "@Transaction/Domain/Events/TransactionWasRefunded";
99
import {TransactionInMemoryProjector} from "@Tests/Transaction/Infrastructure/InMemoryProjector";
1010
import {InMemoryReadModelRepository} from "@Tests/Transaction/Infrastructure/InMemoryReadModelRepository";
11+
import {InMemoryWriteRepository} from "@Tests/Transaction/Infrastructure/InMemoryWriteRepository";
1112
import {EventCollectorListener} from "@Tests/Shared/Infrastructure/EventCollectorListener";
1213
import {InMemoryTransactionRepository} from "@Tests/Transaction/Infrastructure/InMemoryRepository";
1314

@@ -28,6 +29,10 @@ const testServices = new Map([
2829
"infrastructure.transaction.readModel.repository",
2930
{ overwrite: true, instance: InMemoryReadModelRepository },
3031
],
32+
[
33+
"infrastructure.transaction.writeRepository",
34+
{ overwrite: true, instance: InMemoryWriteRepository },
35+
],
3136
[
3237
"infrastructure.orm.readModel.postgresConnection",
3338
{

0 commit comments

Comments
 (0)