Skip to content
This repository has been archived by the owner on Apr 18, 2024. It is now read-only.

Commit

Permalink
Add HelperDecorator
Browse files Browse the repository at this point in the history
Add UnitTests
Add Custom Errors
Improve Exports and Imports
  • Loading branch information
Frank committed Oct 27, 2019
1 parent fa6a2a0 commit 4706af2
Show file tree
Hide file tree
Showing 49 changed files with 1,172 additions and 766 deletions.
8 changes: 5 additions & 3 deletions example/Model/Comment/comment.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@ module.exports = class Comment extends Aggregate {
this.message = event.message;
}

static registeredEvents = [
CommentWasWritten
]
static registeredEvents() {
return [
CommentWasWritten
];
}
}
10 changes: 6 additions & 4 deletions example/Model/User/user.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,10 @@ module.exports = class User extends Aggregate {
this.username = event.username;
}

static registeredEvents = [
UserWasRegistered,
UserNameWasUpdated
]
static registeredEvents() {
return [
UserWasRegistered,
UserNameWasUpdated
];
}
}
9 changes: 6 additions & 3 deletions src/aggregate/aggregate.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { IAggregate } from "./types";
import { IEvent, IEventConstructor } from "../index";
import 'reflect-metadata';
import { IEvent, IAggregate } from "../types";
import { AGGREGATE_CONFIG } from "../decorator/constants";

export abstract class Aggregate implements IAggregate {
protected _recordedEvents: IEvent[] = [];
Expand Down Expand Up @@ -43,5 +44,7 @@ export abstract class Aggregate implements IAggregate {
}, this)
}

public abstract registeredEvents: IEventConstructor[];
static registeredEvents() {
return Reflect.getMetadata(AGGREGATE_CONFIG, this) || [];
}
}
22 changes: 13 additions & 9 deletions src/aggregate/aggregateRepository.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
import {
BaseEvent,
IEventStore,
FieldType, IEvent,
FieldType,
IEvent,
IEventConstructor,
MetadataMatcher,
MetadataOperator
} from "../index";
IMetadataMatcher,
MetadataOperator,
Repository,
RepositoryConfiguration,
IAggregate
} from "../types";

import { Repository, RepositoryConfiguration, IAggregate } from "./types";
import { AggregateNotFound } from "../exception";
import { BaseEvent } from "../event";

interface EventMap {
[name: string]: IEventConstructor
Expand All @@ -21,7 +25,7 @@ export class AggregateRepository<T extends IAggregate> implements Repository<T>
this.eventStore = options.eventStore;
const AggregateConstructor = options.eventStore.eventMap[options.aggregate.name];

const events = (AggregateConstructor || { registeredEvents: [] as Array<IEventConstructor> }).registeredEvents;
const events: IEventConstructor[] = (AggregateConstructor || { registeredEvents: () => [] as Array<IEventConstructor> }).registeredEvents();

this.eventMap = events.reduce<{ [name: string]: IEventConstructor }>((events, event) => {
events[event.name] = event;
Expand All @@ -37,7 +41,7 @@ export class AggregateRepository<T extends IAggregate> implements Repository<T>
}

public async get(aggregateId: string) {
const matcher: MetadataMatcher = {
const matcher: IMetadataMatcher = {
data: [
{ operation: MetadataOperator.EQUALS, field: '_aggregate_id', fieldType: FieldType.METADATA, value: aggregateId }
]
Expand All @@ -48,7 +52,7 @@ export class AggregateRepository<T extends IAggregate> implements Repository<T>
let aggregate: T = new this.options.aggregate();

if (events.length === 0) {
throw new Error(`${aggregate.constructor.name} not found`)
throw AggregateNotFound.withName(aggregate.constructor.name);
}

aggregate.fromHistory(events.map<IEvent>(event => {
Expand Down
2 changes: 2 additions & 0 deletions src/aggregate/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './aggregate';
export * from './aggregateRepository';
8 changes: 6 additions & 2 deletions src/aggregate/types.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
import { IAggregateConstructor, IEventStore, IEvent, IEventConstructor } from "../index";
import { IEventStore, IEvent, IEventConstructor } from "../types";

export interface IAggregateConstructor<T = object> {
new (): T;
registeredEvents(): IEventConstructor[];
}

export interface IAggregate {
popEvents: () => IEvent[]
fromHistory: (events: IEvent[]) => IAggregate
registeredEvents: IEventConstructor[]
}

export interface RepositoryConfiguration<T> {
Expand Down
89 changes: 89 additions & 0 deletions src/decorator/aggregate.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { Aggregate } from "../aggregate/aggregate";
import { BaseEvent } from "../event";
import { AggregateConfig } from "./aggregate";
import { createEventStore } from "../index";
import { InMemoryEventStore } from "../inMemory/eventStore";
import { Driver } from "../types";

class UserWasRegistered extends BaseEvent<{ username: string, password: string }> {
static with(aggregateId, { username, password }) {
return this.occur(aggregateId, { username, password });
}

get userId() {
return this.aggregateId;
}

get username() {
return this.payload.username
}

get password() {
return this.payload.password
}
}

class UserNameWasUpdated extends BaseEvent<{ username: string }> {
static with(aggregateId, { username }) {
return this.occur(aggregateId, { username });
}

get userId() {
return this.aggregateId;
}

get username() {
return this._payload.username
}
}

@AggregateConfig([
UserWasRegistered,
UserNameWasUpdated
])
class User extends Aggregate {
userId = '';
username = '';
password = '';

public static register(userId, username, password) {
const user = new User();

user._recordThat(UserWasRegistered.with(userId, { username, password }))

return user;
}

public changeUsername(username) {
this._recordThat(UserNameWasUpdated.with(this.userId, { username }))

return this;
}

protected _whenUserWasRegistered(event) {
this.userId = event.userId;
this.username = event.username;
this.password = event.password;
}

protected _whenUserNameWasUpdated(event) {
this.username = event.username;
}
}

describe('decorator/aggregate', () => {
it('defines all possible events to the User metadata', (done) => {
expect(User.registeredEvents()).toEqual([UserWasRegistered, UserNameWasUpdated]);

const eventStore = createEventStore({
driver: Driver.IN_MEMORY,
connectionString: ''
}) as InMemoryEventStore;

expect(eventStore.eventMap).toEqual({
[User.name]: User
});

done()
})
});
12 changes: 12 additions & 0 deletions src/decorator/aggregate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { IEventConstructor, IAggregateConstructor } from "../types";
import { AGGREGATE_CONFIG } from "./constants";

const aggregates: IAggregateConstructor[] = [];

export const aggregateMap = () => aggregates;

export const AggregateConfig = (events: IEventConstructor[]) => (target: IAggregateConstructor) => {
Reflect.defineMetadata(AGGREGATE_CONFIG, events, target);

aggregates.push(target)
};
2 changes: 2 additions & 0 deletions src/decorator/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export const AGGREGATE_CONFIG = 'AGGREGATE_CONFIG';
export const PROJECTION_CONFIG = 'PROJECTION_CONFIG';
3 changes: 3 additions & 0 deletions src/decorator/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from './aggregate';
export * from './projection';
export * from './readModelProjection';
116 changes: 116 additions & 0 deletions src/decorator/projection.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import { Aggregate } from "../aggregate/aggregate";
import { BaseEvent } from "../event";
import { AggregateConfig } from "./aggregate";
import { createEventStore } from "../index";
import { InMemoryEventStore } from "../inMemory/eventStore";
import { Projection } from "../projection/projection";
import { ProjectionConfig } from "./projection";
import { IProjectionConstructor } from "../projection/types";
import { Driver } from "../types";

class UserWasRegistered extends BaseEvent<{ username: string, password: string }> {
static with(aggregateId, { username, password }) {
return this.occur(aggregateId, { username, password });
}

get userId() {
return this.aggregateId;
}

get username() {
return this.payload.username
}

get password() {
return this.payload.password
}
}

class UserNameWasUpdated extends BaseEvent<{ username: string }> {
static with(aggregateId, { username }) {
return this.occur(aggregateId, { username });
}

get userId() {
return this.aggregateId;
}

get username() {
return this._payload.username
}
}

@AggregateConfig([
UserWasRegistered,
UserNameWasUpdated
])
class User extends Aggregate {
userId = '';
username = '';
password = '';

public static register(userId, username, password) {
const user = new User();

user._recordThat(UserWasRegistered.with(userId, { username, password }))

return user;
}

public changeUsername(username) {
this._recordThat(UserNameWasUpdated.with(this.userId, { username }))

return this;
}

protected _whenUserWasRegistered(event) {
this.userId = event.userId;
this.username = event.username;
this.password = event.password;
}

protected _whenUserNameWasUpdated(event) {
this.username = event.username;
}
}

@ProjectionConfig('user_list')
class UserListProjection extends Projection<{ [userId: string]: { username: string, id: string, password: string } }> {
async run(keepRunning = false): Promise<Array<{ username: string, id: string, password: string }>> {
await this.projector
.fromStream({ streamName: 'users' })
.init(() => ({}))
.when({
[UserWasRegistered.name]: (state, event: UserWasRegistered) => {
state[event.userId] = { id: event.userId, username: event.username, password: event.password };

return state;
},
[UserNameWasUpdated.name]: (state, event: UserNameWasUpdated) => {
state[event.userId].username = event.username;

return state;
}
})
.run(keepRunning);

return Object.values(this.projector.getState() || {});
}
}


describe('decorator/projection', () => {
it('finds decorated projections and add the defined name to the constructor', (done) => {
const eventStore = createEventStore({
driver: Driver.IN_MEMORY,
connectionString: ''
}) as InMemoryEventStore;

const projection = eventStore.getProjection<UserListProjection>('user_list');

expect(projection).not.toBeNull();
expect((projection.constructor as IProjectionConstructor).projectionName).toEqual('user_list');

done()
})
});
11 changes: 11 additions & 0 deletions src/decorator/projection.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { IProjectionConstructor } from "../types";

const projections: IProjectionConstructor[] = [];

export const projectionMap = () => projections;

export const ProjectionConfig = (name: string) => (target: IProjectionConstructor<any>) => {
target.projectionName = name;

projections.push(target)
};
Loading

0 comments on commit 4706af2

Please sign in to comment.