Comparing version 0.20.1 to 0.21.0
@@ -92,3 +92,3 @@ (function webpackUniversalModuleDefinition(root, factory) { | ||
this._remoteEndpoints = []; | ||
this._globalProjectionClasses = []; | ||
this._globalProjectionObjects = []; | ||
this._globalContext = new GlobalContext; | ||
@@ -126,5 +126,5 @@ this._projectionService = new Projection(this._globalContext); | ||
Eventric.prototype.initializeGlobalProjections = function() { | ||
return Promise.all(this._globalProjectionClasses.map((function(_this) { | ||
return function(GlobalProjectionClass) { | ||
return _this._projectionService.initializeInstance('', new GlobalProjectionClass, {}); | ||
return Promise.all(this._globalProjectionObjects.map((function(_this) { | ||
return function(projectionObject) { | ||
return _this._projectionService.initializeInstance(projectionObject, {}); | ||
}; | ||
@@ -134,4 +134,4 @@ })(this))); | ||
Eventric.prototype.addGlobalProjection = function(ProjectionClass) { | ||
return this._globalProjectionClasses.push(ProjectionClass); | ||
Eventric.prototype.addGlobalProjection = function(projectionObject) { | ||
return this._globalProjectionObjects.push(projectionObject); | ||
}; | ||
@@ -551,6 +551,4 @@ | ||
this._params = {}; | ||
this._projectionClasses = {}; | ||
this._projectionInstances = {}; | ||
this._handlerFunctions = {}; | ||
this.projectionService = new Projection(this); | ||
this._projectionService = new Projection(this); | ||
this.setClient(inmemoryRemote.client); | ||
@@ -599,24 +597,8 @@ this._exposeRpcOperationsAsMemberFunctions(); | ||
Remote.prototype.addProjection = function(projectionName, projectionClass) { | ||
this._projectionClasses[projectionName] = projectionClass; | ||
return this; | ||
}; | ||
Remote.prototype.initializeProjection = function(projectionObject, params) { | ||
return this.projectionService.initializeInstance('', projectionObject, params); | ||
return this._projectionService.initializeInstance(projectionObject, params); | ||
}; | ||
Remote.prototype.initializeProjectionInstance = function(projectionName, params) { | ||
if (!this._projectionClasses[projectionName]) { | ||
return Promise.reject(new Error("Given projection " + projectionName + " not registered on remote")); | ||
} | ||
return this.projectionService.initializeInstance(projectionName, this._projectionClasses[projectionName], params); | ||
}; | ||
Remote.prototype.getProjectionInstance = function(projectionId) { | ||
return this.projectionService.getInstance(projectionId); | ||
}; | ||
Remote.prototype.destroyProjectionInstance = function(projectionId) { | ||
return this.projectionService.destroyInstance(projectionId, this); | ||
return this._projectionService.destroyInstance(projectionId, this); | ||
}; | ||
@@ -710,9 +692,4 @@ | ||
Projection.prototype.initializeInstance = function(projectionName, Projection, params) { | ||
var aggregateId, diFn, diName, eventNames, projection, projectionId, ref; | ||
if (typeof Projection === 'function') { | ||
projection = new Projection; | ||
} else { | ||
projection = Projection; | ||
} | ||
Projection.prototype.initializeInstance = function(projectionObject, params) { | ||
var aggregateId, diFn, diName, eventNames, projectionId, ref; | ||
if (this._context._di) { | ||
@@ -722,3 +699,3 @@ ref = this._context._di; | ||
diFn = ref[diName]; | ||
projection[diName] = diFn; | ||
projectionObject[diName] = diFn; | ||
} | ||
@@ -728,9 +705,9 @@ } | ||
aggregateId = null; | ||
projection.$subscribeHandlersWithAggregateId = function(_aggregateId) { | ||
projectionObject.$subscribeHandlersWithAggregateId = function(_aggregateId) { | ||
return aggregateId = _aggregateId; | ||
}; | ||
eventNames = null; | ||
return this._callInitializeOnProjection(projectionName, projection, params).then((function(_this) { | ||
return this._callInitializeOnProjection(projectionObject, params).then((function(_this) { | ||
return function() { | ||
return _this._parseEventNamesFromProjection(projection); | ||
return _this._parseEventNamesFromProjection(projectionObject); | ||
}; | ||
@@ -740,14 +717,14 @@ })(this)).then((function(_this) { | ||
eventNames = _eventNames; | ||
return _this._applyDomainEventsFromStoreToProjection(projectionId, projection, eventNames, aggregateId); | ||
return _this._applyDomainEventsFromStoreToProjection(projectionId, projectionObject, eventNames, aggregateId); | ||
}; | ||
})(this)).then((function(_this) { | ||
return function() { | ||
return _this._subscribeProjectionToDomainEvents(projectionId, projectionName, projection, eventNames, aggregateId); | ||
return _this._subscribeProjectionToDomainEvents(projectionId, projectionObject, eventNames, aggregateId); | ||
}; | ||
})(this)).then((function(_this) { | ||
return function() { | ||
return _this._projectionInstances[projectionId] = projection; | ||
return _this._projectionInstances[projectionId] = projectionObject; | ||
}; | ||
})(this)).then(function() { | ||
return projection.isInitialized = true; | ||
return projectionObject.isInitialized = true; | ||
}).then(function() { | ||
@@ -758,12 +735,9 @@ return projectionId; | ||
Projection.prototype._callInitializeOnProjection = function(projectionName, projection, params) { | ||
Projection.prototype._callInitializeOnProjection = function(projection, params) { | ||
return new Promise((function(_this) { | ||
return function(resolve, reject) { | ||
if (!projection.initialize) { | ||
logger.debug("[" + _this._context.name + "] No initialize function on Projection " + projectionName + " given, skipping"); | ||
return resolve(projection); | ||
} | ||
logger.debug("[" + _this._context.name + "] Calling initialize on Projection " + projectionName); | ||
return projection.initialize(params, function() { | ||
logger.debug("[" + _this._context.name + "] Finished initialize call on Projection " + projectionName); | ||
return resolve(projection); | ||
@@ -817,3 +791,3 @@ }); | ||
Projection.prototype._subscribeProjectionToDomainEvents = function(projectionId, projectionName, projection, eventNames, aggregateId) { | ||
Projection.prototype._subscribeProjectionToDomainEvents = function(projectionId, projection, eventNames, aggregateId) { | ||
var domainEventHandler, subscribeProjectionToDomainEventsPromise; | ||
@@ -971,3 +945,3 @@ domainEventHandler = (function(_this) { | ||
this._domainEventHandlers = {}; | ||
this._projectionClasses = {}; | ||
this._projectionObjects = []; | ||
this._repositoryInstances = {}; | ||
@@ -977,3 +951,3 @@ this._storeInstance = null; | ||
this._eventBus = new EventBus; | ||
this.projectionService = new Projection(this); | ||
this._projectionService = new Projection(this); | ||
} | ||
@@ -1058,22 +1032,9 @@ | ||
Context.prototype.addProjection = function(projectionName, ProjectionClass) { | ||
this._projectionClasses[projectionName] = ProjectionClass; | ||
Context.prototype.addProjection = function(projectionObject) { | ||
this._projectionObjects.push(projectionObject); | ||
return this; | ||
}; | ||
Context.prototype.addProjections = function(viewsObj) { | ||
var ProjectionClass, projectionName; | ||
for (projectionName in viewsObj) { | ||
ProjectionClass = viewsObj[projectionName]; | ||
this.addProjection(projectionName, ProjectionClass); | ||
} | ||
return this; | ||
}; | ||
Context.prototype.getProjectionInstance = function(projectionId) { | ||
return this.projectionService.getInstance(projectionId); | ||
}; | ||
Context.prototype.destroyProjectionInstance = function(projectionId) { | ||
return this.projectionService.destroyInstance(projectionId, this); | ||
return this._projectionService.destroyInstance(projectionId, this); | ||
}; | ||
@@ -1106,16 +1067,11 @@ | ||
Context.prototype._initializeProjections = function() { | ||
var ProjectionClass, initializeProjectionsPromise, projectionName, ref; | ||
var i, initializeProjectionsPromise, len, projectionObject, ref; | ||
initializeProjectionsPromise = Promise.resolve(); | ||
ref = this._projectionClasses; | ||
for (projectionName in ref) { | ||
ProjectionClass = ref[projectionName]; | ||
logger.debug("[" + this.name + "] Initializing Projection " + projectionName); | ||
ref = this._projectionObjects; | ||
for (i = 0, len = ref.length; i < len; i++) { | ||
projectionObject = ref[i]; | ||
initializeProjectionsPromise = initializeProjectionsPromise.then((function(_this) { | ||
return function() { | ||
return _this.projectionService.initializeInstance(projectionName, ProjectionClass, {}); | ||
return _this._projectionService.initializeInstance(projectionObject, {}); | ||
}; | ||
})(this)).then((function(_this) { | ||
return function(projectionId) { | ||
return logger.debug("[" + _this.name + "] Finished initializing Projection " + projectionName); | ||
}; | ||
})(this)); | ||
@@ -1130,13 +1086,2 @@ } | ||
Context.prototype.initializeProjectionInstance = function(projectionName, params) { | ||
if (!this._projectionClasses[projectionName]) { | ||
return Promise.reject(new Error("Given projection " + projectionName + " not registered on context")); | ||
} | ||
return this.projectionService.initializeInstance(projectionName, this._projectionClasses[projectionName], params); | ||
}; | ||
Context.prototype.getProjection = function(projectionId) { | ||
return this.projectionService.getInstance(projectionId); | ||
}; | ||
Context.prototype.getDomainEventsStore = function() { | ||
@@ -1143,0 +1088,0 @@ return this._storeInstance; |
@@ -14,3 +14,3 @@ { | ||
"author": "efa Team <team@efa-gmbh.com>", | ||
"version": "0.20.1", | ||
"version": "0.21.0", | ||
"repository": { | ||
@@ -17,0 +17,0 @@ "type": "git", |
511
README.md
@@ -1,135 +0,470 @@ | ||
> Not production ready. API might change heavily. | ||
 | ||
## eventric.js [](https://travis-ci.org/efacilitation/eventric) | ||
# eventric.js [](https://travis-ci.org/efacilitation/eventric) | ||
Behavior-first application development. Runs on NodeJS and modern Browsers. | ||
Minimalist JavaScript framework to build applications based on DDD, CQRS and Event Sourcing. | ||
Supports (micro)service based architectures and focuses on high [testability](https://github.com/efacilitation/eventric-testing). | ||
**Note: eventric is currently under heavy development. The readme is outdated. It will be updated soon.** | ||
eventric is written in CoffeeScript. If you need a JavaScript tutorial please compile the snippets below yourself. | ||
## Philosophy | ||
### Current road map | ||
* Emphasize [Domain-driven design](https://www.goodreads.com/book/show/179133.Domain_Driven_Design), [Event-driven architecture](https://www.goodreads.com/book/show/12369902-event-centric) and [Task-based UIs](http://cqrs.wordpress.com/documents/task-based-ui). | ||
* Start with the Behavior of your application and go from there ([BDD](http://dannorth.net/introducing-bdd/)) | ||
* Put the the Domain Model in the very center of your Layered Architecture ([Onion](http://jeffreypalermo.com/blog/the-onion-architecture-part-1/) / [Hexagonal](http://alistair.cockburn.us/Hexagonal+architecture)) | ||
* Explicitly set boundaries for parts of your application ([BoundedContexts](https://en.wikipedia.org/wiki/Domain-driven_design#Bounded_context) / [MicroServices](http://martinfowler.com/articles/microservices.html)) | ||
* Separation of concerns using Commands and Queries ([CQRS](http://msdn.microsoft.com/en-us/library/jj554200.aspx)) | ||
* Capture all changes to your application state as a sequence of [DomainEvents](http://www.udidahan.com/2009/06/14/domain-events-salvation/) ([EventSourcing](http://martinfowler.com/eaaDev/EventSourcing.html)) | ||
* Be reactive ([Manifesto](http://www.reactivemanifesto.org)) | ||
Currently there is an event store implementation for MongoDB which **will not work correctly in a multi process scenario.** | ||
We will soon be working on an event store implementation for | ||
[http://geteventstore.com](http://geteventstore.com) which will get rid of this limitation. | ||
## Getting started | ||
Implementations for other databases are currently not planned. | ||
## Tutorial | ||
This tutorial will guide you through all features of eventric by implementing a simplified Todo application. | ||
There is no API documentation. If you want to dig deeper, have a look at the source code and the specs. | ||
### Installation | ||
Install the framework inside your application with `npm install eventric`. | ||
eventric requires [Promises](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise). | ||
If you have to support older browsers install and use a polyfill, such as | ||
[es6-promise](https://github.com/jakearchibald/es6-promise) or [rsvp.js](https://github.com/tildeio/rsvp.js). | ||
### Context | ||
First create a Todo context inside your application. | ||
```coffeescript | ||
todoContext = eventric.context 'Todo' | ||
``` | ||
npm install eventric | ||
Contexts create architectural boundaries inside an eventric based application. | ||
They can be compared to (micro)services and somewhat also to bounded contexts from an implementation perspective. | ||
A context holds its own event store and provides a self contained space for domain events, aggregates, | ||
command and query handlers, projections and an event publishing infrastructure (which may be outsourced soon). | ||
### Domain events | ||
After creating the context the necessary domain events can be defined. | ||
```coffeescript | ||
todoContext.defineDomainEvents | ||
TodoCreated: ({title}) -> | ||
@title = title | ||
TodoTitleChanged: ({title}) -> | ||
@title = title | ||
TodoFinished: -> | ||
``` | ||
**Important:** eventric requires [Promises](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise). | ||
If you have to support older browsers use the [es6-promise](https://github.com/jakearchibald/es6-promise) polyfill. | ||
Domain event definitions in eventric consist of two parts: The domain event name and the payload constructor function. | ||
This definition is similar to a class used in statically typed languages such as Java or C#. | ||
Inside the payload constructor function the expected values must be assigned to members of `this`. | ||
### Setup Context | ||
*Note:* It is best practice to use the same name for parameters and assigned member variables. | ||
Having discussed the upcoming **TodoApp Project** with the Business-Experts and fellow Developers it got clear that we should start with a `Context` named `Todo`. | ||
```javascript | ||
eventric = require('eventric'); | ||
### Aggregates | ||
todoContext = eventric.context('Todo'); | ||
Domain events in eventric can only be emitted from inside aggregate instances. | ||
An aggregate is defined through a plain class which must at least implement a `create()` function. | ||
This function will be automatically called when creating a new aggregate from inside a command handler. | ||
Use the injected function `$emitDomainEvent(eventName, eventPayload)` to emit a new domain event. | ||
This will cause two things to happen: | ||
- The new domain event is saved in the list of new domain events | ||
- If existing, the correct handle function on the aggregate is executed (`handle<event name>()`) | ||
The above mentioned handle functions are also used when loading and rebuilding an aggregate to create the current state. | ||
```coffeescript | ||
todoContext.addAggregate 'Todo', class Todo | ||
create: ({title}) -> | ||
if not title | ||
throw new Error 'title missing' | ||
@$emitDomainEvent 'TodoCreated', | ||
title: title | ||
change: ({title}) -> | ||
if not title | ||
throw new Error 'title missing' | ||
if @finished | ||
throw new Error 'todo already finished' | ||
@$emitDomainEvent 'TodoTitleChanged', | ||
title: title | ||
finish: -> | ||
@$emitDomainEvent 'TodoFinished' | ||
handleTodoFinished: -> | ||
@isFinished = true | ||
``` | ||
*Note:* Aggregate functions (except for handle functions) can return promises. eventric will wait for them to resolve. | ||
### Define the Event | ||
Inside of our `Todo` Context things will happen which are called DomainEvents. A technique to come up with these is called [EventStorming](http://ziobrando.blogspot.co.uk/2013/11/introducing-event-storming.html). Lets add two called `TodoCreated` and `TodoDescriptionChanged`. | ||
## Command handlers | ||
```javascript | ||
todoContext.defineDomainEvents({ | ||
TodoCreated: function(params) {}, | ||
TodoDescriptionChanged: function(params) { | ||
this.description = params.description; | ||
} | ||
}); | ||
Command handlers define the write side of the application service layer inside a context. | ||
### Definition | ||
Command handlers are registered by passing an object to the context where the keys define the command names. | ||
```coffeescript | ||
todoContext.addCommandHandlers | ||
CreateTodo: ({title}) -> | ||
@$aggregate.create 'Todo', | ||
title: title | ||
.then (todoId) -> | ||
return todoId | ||
ChangeTodoTitle: ({todoId, title}) -> | ||
@$aggregate.load 'Todo', todoId | ||
.then (todo) -> | ||
todo.changeTitle title | ||
todo.$save() | ||
FinishTodo: ({todoId}) -> | ||
@$aggregate.load 'Todo', todoId | ||
.then (todo) -> | ||
todo.finish() | ||
todo.$save() | ||
``` | ||
Use the injected service `$aggregate` to create, load, modify and save aggregate instances. | ||
Creating an aggregate will cause the `create()` function defined on the aggregate class to be called. | ||
Execute the injected function `$save()` to save new domain events and publish them via the event bus. | ||
### Adding an Aggregate | ||
Although discouraged, queries can be executed by using the injected service `$query`. | ||
Now we need an Aggregate which actually raises this DomainEvents. | ||
### Execution | ||
```javascript | ||
todoContext.addAggregate('Todo', function() { | ||
this.create = function() { | ||
this.$emitDomainEvent('TodoCreated'); | ||
} | ||
this.changeDescription = function(description) { | ||
this.$emitDomainEvent('TodoDescriptionChanged', {description: description}); | ||
} | ||
}); | ||
After defining the command handlers they can be executed from outside the context. | ||
In order to work with a context it needs to be initialized. | ||
The initialization is mainly required for projections. | ||
```coffeescript | ||
todoContext.initialize() | ||
.then -> | ||
todoContext.command 'CreateTodo', title: 'My first todo' | ||
.then (todoId) -> | ||
todoContext.command 'ChangeTodoTitle', | ||
todoId: todoId | ||
title: 'My first changed todo' | ||
.then -> | ||
todoContext.command 'FinishTodo', | ||
todoId: todoId | ||
.then -> | ||
console.log 'todo created, changed and finished' | ||
``` | ||
> Hint: `this.create` is called by convention when you create an aggregate using `this.$aggregate.create` | ||
> Hint: `this.$emitDomainEvent` is dependency injected | ||
## Domain event handler | ||
### Adding CommandHandlers | ||
Domain event handlers can be registered for specific events and even for specific aggregate instances. | ||
To actually work with the `Context` from the outside world we need `CommandHandlers`. Let's start by adding a simple one that will create an instance of our `Todo` Aggregate. | ||
```coffeescript | ||
todoContext.subscribeToDomainEvent 'TodoFinished', (domainEvent) -> | ||
console.log 'finished todo', domainEvent.aggregate.id | ||
```javascript | ||
todoContext.addCommandHandlers({ | ||
CreateTodo: function(params) { | ||
return this.$aggregate.create('Todo') | ||
.then(function (todo) { | ||
return todo.$save(); | ||
}); | ||
} | ||
}); | ||
todoContext.subscribeToDomainEventWithAggregateId 'TodoTitleChanged', 'some aggregate id', (domainEvent) -> | ||
console.log 'change title to: ', domainEvent.payload.title | ||
``` | ||
> Hint: `this.$aggregate` is dependency injected | ||
It would be nice if we could change the description of the `Todo`, so let's add this `CommandHandler` too. | ||
## Projection | ||
```javascript | ||
todoContext.addCommandHandlers({ | ||
ChangeTodoDescription: function(params) { | ||
return this.$aggregate.load('Todo', params.id) | ||
.then(function (todo) { | ||
todo.changeDescription(params.description); | ||
return todo.$save(); | ||
}); | ||
} | ||
}); | ||
Projections always replay an event stream from the beginning. | ||
They are used to create or populate read models. | ||
```coffeescript | ||
todosReadModel = {} | ||
todosProjection = | ||
initialize: (params, done) -> | ||
done() | ||
handleTodoCreated: (domainEvent) -> | ||
todosReadModel[domainEvent.aggregate.id] = | ||
title: domainEvent.payload.title | ||
handleTodoTitleChanged: (domainEvent) -> | ||
todosReadModel[domainEvent.aggregate.id].title = domainEvent.payload.title | ||
handleTodoFinished: (domainEvent) -> | ||
todosReadModel[domainEvent.aggregate.id].isFinished = true | ||
todoContext.addProjection todosProjection | ||
todoCountReadModel = 0 | ||
todoCountProjection = | ||
initialize: (params, done) -> | ||
done() | ||
handleTodoCreated: (domainEvent) -> | ||
todoCountReadModel++ | ||
todoContext.addProjection todoCountProjection | ||
``` | ||
**Important:** Projections must be added to a context before it is initialized. | ||
### Subscribe to a DomainEvent | ||
And last but not least we want to console.log when the description of the `Todo` changes. | ||
## Query handlers | ||
```javascript | ||
todoContext.subscribeToDomainEvent('TodoDescriptionChanged', function(domainEvent) { | ||
console.log(domainEvent.payload.description); | ||
}); | ||
Query handlers define the read side of the application service layer inside an eventric context. | ||
### Definition | ||
Query handlers are registered the same way command handlers are by passing an object to the context. | ||
```coffeescript | ||
todoContext.addQueryHandlers | ||
getTodos: (params) -> | ||
return todosReadModel | ||
getTodoCount: (params) -> | ||
return todoCountReadModel | ||
``` | ||
### Execution | ||
### Executing Commands | ||
Similar to command handlers queries can be executed from outside the context after defining them. | ||
Initialize the Context, create a `Todo` and tell the `Todo` to change its description. | ||
```javascript | ||
```coffeescript | ||
todoContext.initialize() | ||
.then(function() { | ||
return todoContext.command('CreateTodo'); | ||
}) | ||
.then(function(todoId) { | ||
return todoContext.command('ChangeTodoDescription', { | ||
id: todoId, | ||
description: 'Do something' | ||
}); | ||
}); | ||
.then -> | ||
todoContext.command 'CreateTodo', title: 'My first todo' | ||
.then (todoId) -> | ||
todoContext.command 'ChangeTodoTitle', | ||
todoId: todoId | ||
title: 'My first changed todo' | ||
.then -> | ||
todoContext.command 'FinishTodo', | ||
todoId: todoId | ||
.then -> | ||
todoContext.query 'getTodoList', {} | ||
.then (todoList) -> | ||
console.log 'current todos:', todoList | ||
todoContext.query 'getTodoCount' | ||
.then (todoCount) -> | ||
console.log 'current todo count:', todoCount | ||
``` | ||
After executing the Commands the DomainEventHandler will print `Do something`. Your `Todo` Aggregate is now persisted using EventSourcing into the `InMemory Store`. | ||
## Stores | ||
The event store inside a context is responsible for saving domain events and searching them by aggregate id or event name. | ||
### In memory | ||
By default eventric uses an in memory event store which is mainly useful for demo applications and testing purposes. | ||
### MongoDB | ||
For actual applications use the mongodb event store to save domain events in a persistent way. | ||
First, install it together with the mongodb module. | ||
`npm install mongodb` | ||
`npm install eventric-store-mongodb` | ||
Then, before initializing any contexts, connect to the database and set the mongodb event store as eventric store. | ||
```coffeescript | ||
mongodb = require 'mongodb' | ||
EventricMongoDBStore = require 'eventric-store-mongodb' | ||
mongodb.MongoClient.connect 'your db url', (error, database) -> | ||
eventric.setStore EventricMongoDBStore, dbInstance: database | ||
# initialize todo context | ||
``` | ||
## Persistent read models | ||
An event store inside a context only handles domain event persistence (write side of the application). | ||
Saving persistent read models (or views) are not scope of the eventric framework. | ||
To illustrate how this can be done the above todo list projection is rewritten to use mongodb as read model store. | ||
``` | ||
todoListProjection = | ||
initialize: (params, done) -> | ||
database.dropCollection 'todos' | ||
.then -> | ||
database.collection 'todos' | ||
.then (collection) -> | ||
@collection = collection | ||
done() | ||
handleTodoCreated: (domainEvent) -> | ||
@collection.insert | ||
id: domainEvent.aggregate.id | ||
title: domainEvent.payload.title | ||
handleTodoTitleChanged: (domainEvent) -> | ||
@collection.update id: domainEvent.aggregate.id, | ||
$set: title: domainEvent.payload.title | ||
handleTodoFinished: (domainEvent) -> | ||
@collection.update id: domainEvent.aggregate.id, | ||
$set: isFinished: true | ||
``` | ||
*Note:* The above will cause the read model to be emptied whenever the process is restarted. Consider this a best practice. | ||
The query handler can be changed accordingly to directly access the mongodb collection. | ||
```coffeescript | ||
todoContext.addQueryHandlers | ||
getTodos: -> | ||
database.collection 'todos' | ||
.then (collection) -> | ||
collection.find({}).toArray() | ||
``` | ||
## Remotes | ||
eventric supports service oriented architectures. | ||
Consider every context to be a possible standalone (micro)service. | ||
All contexts share the same API: commands, queries, domain event handlers and projections. | ||
Use the `Remote` interface in order to communicate between contexts. | ||
### In memory | ||
By default eventric provides an in memory remote which is useful for in-process communication and testing purposes. | ||
```coffeescript | ||
todoContext = eventric.remote 'Todo' | ||
todoContext.command 'CreateTodo' | ||
.then (todoId) -> | ||
console.log todoId | ||
``` | ||
### Socket.IO | ||
All previous examples were meant to be executed in a single process on the server side of an application. | ||
In order to communicate with a context running on a server from a browser use the Socket.IO remote implementations. | ||
First, install the modules together with Socket.IO. | ||
``` | ||
npm install socket.io | ||
npm install eventric-remote-socketio-endpoint | ||
npm install eventric-remote-socketio-client | ||
``` | ||
Then, configure eventric on the server side to use Socket.IO as additional remote endpoint. | ||
```coffeescript | ||
socketIO = require 'socket.io' | ||
socketIORemoteEndpoint = require 'eventric-remote-socketio-endpoint' | ||
io = socketIO.listen() | ||
socketIORemoteEndpoint.initialize socketIORemoteEndpointOptions, -> | ||
eventric.addRemoteEndpoint ioInstance: io | ||
``` | ||
Finally, include the Socket.IO client and eventric in an html file, configure the remote client and create a remote. | ||
```html | ||
<html> | ||
<head> | ||
<title>eventric demo</title> | ||
</head> | ||
<body> | ||
<!-- fix paths to files --> | ||
<script type="text/javascript" href="path/to/socket.io/client.js" ></script> | ||
<script type="text/javascript" href="path/to/eventric/dist/release/eventric.js" ></script> | ||
<script type="text/javascript"> | ||
var socket = io(); | ||
socketIORemoteClient = window['eventric-remote-socketio-client']: | ||
socketIORemoteClient.initialize({ | ||
ioClientInstance: socket | ||
}) | ||
.then(function() { | ||
todoContext = eventric.remote('Todo'); | ||
todoContext.setClient(socketIORemoteClient); | ||
todoContext.command('CreateTodo', {}, function(todoId) { | ||
console.log(todoId); | ||
}); | ||
}); | ||
</script> | ||
</body> | ||
</html> | ||
``` | ||
*Note:* Socket.IO remotes are not limited to browser to server. They can easily be used for server to server communication. | ||
### Remote projections | ||
One major strength of eventric is the possibility to create remote projections (or client side projections). | ||
This feature makes it easily possible to create reactive user interfaces in the browser. | ||
```coffeescript | ||
todoContext = eventric.remote 'Todo' | ||
todoContext.setClient socketIORemoteClient | ||
todoProjection = | ||
initialize: (params, done) -> | ||
document.querySelector('body').innerHTML = '<h1>Todos</h1><div class="todos"></div>'; | ||
done() | ||
handleTodoCreated: (domainEvent) -> | ||
todoElement = document.createElement 'div' | ||
todoElement.setAttribute 'id', domainEvent.aggregate.id | ||
todoElement.innerHTML = domainEvent.payload.title | ||
document.querySelector('.todos').appendChild todoElement | ||
handleTodoTitleChanged: (domainEvent) -> | ||
document.querySelector("[id='#{domainEvent.aggregate.id}']").innerHTML = domainEvent.payload.title | ||
handleTodoFinished: (domainEvent) -> | ||
document.querySelector("[id='#{domainEvent.aggregate.id}']").setAttribute 'style', 'text-decoration: line-through' | ||
todoContext.initializeProjection todoProjection, {} | ||
``` | ||
*Note:* `initializeProjection()` may be renamed to `addProjection()` in order to stay consistent with the context API. | ||
## License | ||
@@ -139,2 +474,2 @@ | ||
Copyright (c) 2013-2015 SixSteps Team, eFa GmbH | ||
Copyright (c) 2013-2015 SixSteps Team, efa GmbH |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
475
705475
15345