Skip to content

Commit

Permalink
Merge pull request #31 from NickTsitlakidis/improvements
Browse files Browse the repository at this point in the history
Improvements
  • Loading branch information
NickTsitlakidis authored Jun 4, 2024
2 parents d9bc21e + dd9c4af commit e552bce
Show file tree
Hide file tree
Showing 24 changed files with 314 additions and 305 deletions.
15 changes: 12 additions & 3 deletions .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,11 @@
},
{
"files": ["*.ts"],
"extends": ["plugin:@nx/typescript", "plugin:sonarjs/recommended", "plugin:perfectionist/recommended-natural"],
"extends": [
"plugin:@nx/typescript",
"plugin:sonarjs/recommended",
"plugin:perfectionist/recommended-natural"
],
"rules": {
"sonarjs/no-duplicate-string": "warn",
"perfectionist/sort-classes": [
Expand All @@ -44,13 +48,18 @@
"private-method"
]
}
]
],
"@typescript-eslint/no-extra-semi": "error",
"no-extra-semi": "off"
}
},
{
"files": ["*.js", "*.jsx"],
"extends": ["plugin:@nx/javascript"],
"rules": {}
"rules": {
"@typescript-eslint/no-extra-semi": "error",
"no-extra-semi": "off"
}
},
{
"files": ["*.spec.ts", "*.spec.tsx", "*.spec.js", "*.spec.jsx"],
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ jobs:

strategy:
matrix:
node-version: [16.x]
node-version: [18.x]

steps:
- name: Checkout
Expand Down
18 changes: 9 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ An event is a representation of something that has happened in the past. It is i
Each event serves three purposes :
* It will be persisted so that it can be used to reconstruct the state of an aggregate root
* It will be passed to any internal subscribers that need to react to this event (e.g. updating the read model)
* When it's time to recreate the aggregate root, the event will be processed by the correct method in the aggregate root
* When it's time to recreate the aggregate root, the event will be applied by the correct method in the aggregate root

There is no specific requirement for the structure of an event, but it is recommended to keep it simple and immutable. The [class-transformer](https://github.com/typestack/class-transformer) library is utilized under the hood to save and read the events from the database. Therefore, your event classes should adhere to the rules of class-transformer to be properly serialized and deserialized.

Expand Down Expand Up @@ -175,7 +175,7 @@ export class UserUpdatedEvent {
We start this example by defining two simple events for a user: a creation event and an update event. Each one has its own data, and they are identified by a unique name which is set with the `@DomainEvent` decorator.

```typescript
import { AggregateRoot, AggregateRootName, EventProcessor, StoredEvent } from "@event-nest/core";
import { AggregateRoot, AggregateRootName, ApplyEvent, StoredEvent } from "@event-nest/core";

@AggregateRootName("User")
export class User extends AggregateRoot {
Expand All @@ -189,7 +189,7 @@ export class User extends AggregateRoot {
public static createNew(id: string, name: string, email: string): User {
const user = new User(id);
const event = new UserCreatedEvent(name, email);
user.processUserCreatedEvent(event);
user.applyUserCreatedEvent(event);
user.append(event);
return user;
}
Expand All @@ -202,18 +202,18 @@ export class User extends AggregateRoot {

public update(newName: string) {
const event = new UserUpdatedEvent(newName);
this.processUserUpdatedEvent(event);
this.applyUserUpdatedEvent(event);
this.append(event);
}

@EventProcessor(UserCreatedEvent)
private processUserCreatedEvent = (event: UserCreatedEvent) => {
@ApplyEvent(UserCreatedEvent)
private applyUserCreatedEvent = (event: UserCreatedEvent) => {
this.name = event.name;
this.email = event.email;
};

@EventProcessor(UserUpdatedEvent)
private processUserUpdatedEvent = (event: UserUpdatedEvent) => {
@ApplyEvent(UserUpdatedEvent)
private applyUserUpdatedEvent = (event: UserUpdatedEvent) => {
this.name = event.newName;
};

Expand All @@ -232,7 +232,7 @@ In our case, we have the following creation cases :

The `reconstitute` method will initiate the event processing based on the events order.

To process each event, we have defined two private methods which are decorated with the `@EventProcessor` decorator. Each method will be called when the corresponding event is retrieved, and it's ready to be processed.
To apply each event, we have defined two private methods which are decorated with the `@ApplyEvent` decorator. Each method will be called when the corresponding event is retrieved, and it's ready to be processed.
This is the place to update the object's internal state based on the event's data. **Make sure that these methods are defined as arrow functions, otherwise they won't be called.**


Expand Down
8 changes: 2 additions & 6 deletions apps/example/project.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "apps/example/src",
"projectType": "application",
"tags": [],
"targets": {
"build": {
"executor": "@nx/webpack:webpack",
Expand Down Expand Up @@ -38,17 +39,12 @@
}
}
},
"lint": {
"executor": "@nx/eslint:lint",
"outputs": ["{options.outputFile}"]
},
"test": {
"executor": "@nx/jest:jest",
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
"options": {
"jestConfig": "apps/example/jest.config.ts"
}
}
},
"tags": []
}
}
6 changes: 3 additions & 3 deletions apps/example/src/app/user/user.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { AggregateRoot, AggregateRootName, EventProcessor, StoredEvent } from "@event-nest/core";
import { AggregateRoot, AggregateRootName, ApplyEvent, StoredEvent } from "@event-nest/core";

import { UserCreatedEvent, UserUpdatedEvent } from "./user-events";

Expand All @@ -7,13 +7,13 @@ export class User extends AggregateRoot {
private _email: string;
private _name: string;

@EventProcessor(UserCreatedEvent)
@ApplyEvent(UserCreatedEvent)
private processUserCreatedEvent = (event: UserCreatedEvent) => {
this._name = event.name;
this._email = event.email;
};

@EventProcessor(UserUpdatedEvent)
@ApplyEvent(UserUpdatedEvent)
private processUserUpdatedEvent = (event: UserUpdatedEvent) => {
this._name = event.newName;
};
Expand Down
18 changes: 9 additions & 9 deletions libs/core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ An event is a representation of something that has happened in the past. It is i
Each event serves three purposes :
* It will be persisted so that it can be used to reconstruct the state of an aggregate root
* It will be passed to any internal subscribers that need to react to this event (e.g. updating the read model)
* When it's time to recreate the aggregate root, the event will be processed by the correct method in the aggregate root
* When it's time to recreate the aggregate root, the event will be applied by the correct method in the aggregate root

There is no specific requirement for the structure of an event, but it is recommended to keep it simple and immutable. The [class-transformer](https://github.com/typestack/class-transformer) library is utilized under the hood to save and read the events from the database. Therefore, your event classes should adhere to the rules of class-transformer to be properly serialized and deserialized.

Expand Down Expand Up @@ -175,7 +175,7 @@ export class UserUpdatedEvent {
We start this example by defining two simple events for a user: a creation event and an update event. Each one has its own data, and they are identified by a unique name which is set with the `@DomainEvent` decorator.

```typescript
import { AggregateRoot, AggregateRootName, EventProcessor, StoredEvent } from "@event-nest/core";
import { AggregateRoot, AggregateRootName, ApplyEvent, StoredEvent } from "@event-nest/core";

@AggregateRootName("User")
export class User extends AggregateRoot {
Expand All @@ -189,7 +189,7 @@ export class User extends AggregateRoot {
public static createNew(id: string, name: string, email: string): User {
const user = new User(id);
const event = new UserCreatedEvent(name, email);
user.processUserCreatedEvent(event);
user.applyUserCreatedEvent(event);
user.append(event);
return user;
}
Expand All @@ -202,18 +202,18 @@ export class User extends AggregateRoot {

public update(newName: string) {
const event = new UserUpdatedEvent(newName);
this.processUserUpdatedEvent(event);
this.applyUserUpdatedEvent(event);
this.append(event);
}

@EventProcessor(UserCreatedEvent)
private processUserCreatedEvent = (event: UserCreatedEvent) => {
@ApplyEvent(UserCreatedEvent)
private applyUserCreatedEvent = (event: UserCreatedEvent) => {
this.name = event.name;
this.email = event.email;
};

@EventProcessor(UserUpdatedEvent)
private processUserUpdatedEvent = (event: UserUpdatedEvent) => {
@ApplyEvent(UserUpdatedEvent)
private applyUserUpdatedEvent = (event: UserUpdatedEvent) => {
this.name = event.newName;
};

Expand All @@ -232,7 +232,7 @@ In our case, we have the following creation cases :

The `reconstitute` method will initiate the event processing based on the events order.

To process each event, we have defined two private methods which are decorated with the `@EventProcessor` decorator. Each method will be called when the corresponding event is retrieved, and it's ready to be processed.
To apply each event, we have defined two private methods which are decorated with the `@ApplyEvent` decorator. Each method will be called when the corresponding event is retrieved, and it's ready to be processed.
This is the place to update the object's internal state based on the event's data. **Make sure that these methods are defined as arrow functions, otherwise they won't be called.**


Expand Down
8 changes: 2 additions & 6 deletions libs/core/project.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "libs/core/src",
"projectType": "library",
"tags": ["publishable"],
"targets": {
"build": {
"executor": "@nx/js:tsc",
Expand All @@ -20,10 +21,6 @@
"command": "node tools/scripts/publish.mjs core {args.ver} {args.tag}",
"dependsOn": ["build"]
},
"lint": {
"executor": "@nx/eslint:lint",
"outputs": ["{options.outputFile}"]
},
"test": {
"executor": "@nx/jest:jest",
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
Expand All @@ -34,6 +31,5 @@
"codeCoverage": true
}
}
},
"tags": ["publishable"]
}
}
2 changes: 1 addition & 1 deletion libs/core/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
export * from "./lib/aggregate-root/aggregate-root";
export * from "./lib/aggregate-root/aggregate-root-name";
export * from "./lib/aggregate-root/event-processor";
export * from "./lib/aggregate-root/apply-event.decorator";

export * from "./lib/exceptions/event-name-conflict-exception";
export * from "./lib/exceptions/unknown-event-exception";
Expand Down
30 changes: 24 additions & 6 deletions libs/core/src/lib/aggregate-root/aggregate-root.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { UnregisteredEventException } from "../exceptions/unregistered-event-exc
import { StoredEvent } from "../storage/stored-event";
import { AggregateRoot } from "./aggregate-root";
import { AggregateRootEvent } from "./aggregate-root-event";
import { EventProcessor } from "./event-processor";
import { ApplyEvent } from "./apply-event.decorator";

@DomainEvent("test-event-1")
class TestEvent1 {}
Expand All @@ -20,13 +20,13 @@ class ThrowingEvent {}
class UnregisteredEvent {}

class SubEntity extends AggregateRoot {
@EventProcessor(TestEvent1)
@ApplyEvent(TestEvent1)
processTestEvent1 = () => {};

@EventProcessor(TestEvent2)
@ApplyEvent(TestEvent2)
processTestEvent2 = () => {};

@EventProcessor(ThrowingEvent)
@ApplyEvent(ThrowingEvent)
processThrowingEvent = () => {
throw new Error("ooops");
};
Expand Down Expand Up @@ -75,7 +75,7 @@ describe("constructor tests", () => {
});

describe("reconstitute tests", () => {
test("calls mapped processors after sorting", () => {
test("calls mapped apply methods after sorting", () => {
const ev1 = StoredEvent.fromStorage("ev1", "id1", "test-event-2", new Date(), 10, "ag-name", {});
const ev2 = StoredEvent.fromStorage("ev2", "id1", "test-event-1", new Date(), 2, "ag-name", {});

Expand All @@ -100,7 +100,7 @@ describe("reconstitute tests", () => {
expect(last).toBe(1);
});

test("throws when an event processor throws", () => {
test("throws when an event applier throws", () => {
const ev1 = StoredEvent.fromStorage("ev1", "id1", "throwing-event", new Date(), 10, "ag-name", {});
const entity = new SubEntity("id1");
expect(() => entity.reconstitute([ev1])).toThrow();
Expand Down Expand Up @@ -189,6 +189,24 @@ describe("commit tests", () => {
expect((result as SubEntity).published).toEqual([]);
});

test("does not clear appended events if commit fails", async () => {
const entity = new SubEntity("entity-id");
const event1 = new TestEvent2();
const event2 = new TestEvent2();
entity.append(event1);
entity.append(event2);

entity.publish = () => Promise.reject("error");

try {
await entity.commit();
expect(fail("Should have thrown"));
} catch (error) {
expect(entity.appendedEvents.length).toBe(2);
expect((entity as SubEntity).published).toEqual([]);
}
});

test("publishes and clears appended events", async () => {
const entity = new SubEntity("entity-id");
const event1 = new TestEvent2();
Expand Down
11 changes: 6 additions & 5 deletions libs/core/src/lib/aggregate-root/aggregate-root.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { UnregisteredEventException } from "../exceptions/unregistered-event-exc
import { StoredEvent } from "../storage/stored-event";
import { isNil } from "../utils/type-utils";
import { AggregateRootEvent } from "./aggregate-root-event";
import { getDecoratedPropertyKey } from "./event-processor";
import { getDecoratedPropertyKey } from "./reflection";

type KnownEvent = {
payload: unknown;
Expand Down Expand Up @@ -83,13 +83,14 @@ export abstract class AggregateRoot {
* handlers will be called to take care of async updates.
* Call this once all the events you want, have been appended.
*/
commit(): Promise<AggregateRoot> {
async commit(): Promise<AggregateRoot> {
const toPublish = this._appendedEvents.slice(0);
this._appendedEvents = [];
if (toPublish.length > 0) {
return this.publish(toPublish).then(() => Promise.resolve(this));
await this.publish(toPublish);
this._appendedEvents = [];
return this;
}
return Promise.resolve(this);
return this;
}

// eslint-disable-next-line @typescript-eslint/no-unused-vars
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { AggregateRoot } from "./aggregate-root";
import { EventProcessor, getDecoratedPropertyKey } from "./event-processor";
import { ApplyEvent } from "./apply-event.decorator";
import { getDecoratedPropertyKey } from "./reflection";

class TestEvent {}

Expand All @@ -16,7 +17,7 @@ class TestEntity extends AggregateRoot {
super("id");
}

@EventProcessor(TestEvent)
@ApplyEvent(TestEvent)
processTestEvent() {
// do something
}
Expand Down
23 changes: 23 additions & 0 deletions libs/core/src/lib/aggregate-root/apply-event.decorator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import "reflect-metadata";
// eslint-disable-next-line perfectionist/sort-imports
import { ClassConstructor } from "class-transformer";

import { APPLY_EVENT_DECORATOR_KEY } from "../metadata-keys";

/**
* A decorator to mark that a method is used to apply a specific event to an aggregate root.
* When an aggregate root has to be reconstituted based on persisted events, these methods
* are called to process the events.
*
* @param eventClass The class of the event to be applied.
* @constructor
*/
export function ApplyEvent(eventClass: ClassConstructor<unknown>): PropertyDecorator {
return (propertyParent, propertyKey) => {
Reflect.defineMetadata(
APPLY_EVENT_DECORATOR_KEY + "-" + propertyKey.toString(),
{ eventClass: eventClass, key: propertyKey },
propertyParent
);
};
}
Loading

0 comments on commit e552bce

Please sign in to comment.