A simple filebased eventsourced store
To start using EventStore, you will first need to create 3 things:
In this tutorial, we’ll create seperate artifacts for each these three things.
In this tutorial we’re creating a small membership database. The main functionality will be to register members, change their details etc.
The domain model has the methods on it to “do the business”. The functionality available to the user of the eventstore is represented on the domain model.
When the domain model needs to persist data, an event is dispatched (by calling dispatch(eventName, eventData)
) to write an event to the event log, and consequently handle it in the log aggregator.
When the domain model needs to retrieve data, it gets it from the storage model. The storage model uses the event log the recreate the data model when it is needed.
The following are the methods and properties on the domain model for our membership database:
MemberListDomainModel: class
function MemberListDomainModel(dispatch, logAggregatorData) {
this.registerNewMember = function(member) {
dispatch("newMemberRegistered", { member });
console.log("SEND MAIL -> welcome to new member");
}
this.endMembership = function(name) {
dispatch("membershipEnded", { name });
console.log("SEND MAIL -> goodbye to member");
}
this.correctAddress = function(name, address) {
dispatch("addressCorrected", { name, address });
}
this.memberHasMoved = function(name, address) {
dispatch("memberHasMoved", { name, address });
}
this.listMembers = function() {
let members = logAggregatorData;
let ret = Object.keys(members).map(key => Object.assign({ name: key }, members[key]));
return ret;
}
this.getMember = function(name) {
let members = logAggregatorData;
return members[name];
}
}
Everytime data is persisted, it is written to the event log. Its hard to use data from the eventlog directly, so the log aggregator is used to replay the events from the event log to build a more comprehensible data model. See the Patterns page for a description of different patterns for building a model from a log.
The user of the eventstore will not see the log aggregator directly, but will instead interact with the domain model.
Code in the log aggregator must not do external integrations, as the event handlers on the log aggregator will be called for each event each time the log is played back. External integrations (like sending email in this case) should happen in the domain model.
The following are the methods and properties on the log aggregator for our membership database:
MemberListLogAggregator: class
function MemberListLogAggregator(logAggregatorData) {
this.eventHandlers = {
onNewMemberRegistered(eventdata) {
if (logAggregatorData[eventdata.member.name]) {
throw new Error(`onNewMemberRegistered failed. ${eventdata.member.name} is already a member.`)
}
logAggregatorData[eventdata.member.name] = {
address: eventdata.member.address,
membershipLevel: eventdata.member.membershipLevel
};
},
onMembershipEnded(eventdata) {
if (!logAggregatorData[eventdata.name]) {
throw new Error(`onMembershipEnded failed. ${eventdata.name} is not a member.`)
}
delete logAggregatorData[eventdata.name];
},
onAddressCorrected(eventdata) {
if (!logAggregatorData[eventdata.name]) {
throw new Error(`onAddressCorrected failed. ${eventdata.name} is not a member.`)
}
logAggregatorData[eventdata.name].address = eventdata.address;
},
onMemberHasMoved(eventdata) {
if (!logAggregatorData[eventdata.name]) {
throw new Error(`onMemberHasMoved failed. ${eventdata.name} is not a member.`)
}
logAggregatorData[eventdata.name].address = eventdata.address;
}
}
}
The model definition object tells Eventstore how to initialize the domain model and the log aggregator.
The following are the methods and properties on the model definition:
modelDefinition: object
More documentation on all methods and properties on the model definition
let modelDefinition = {
snapshotName: "memberlist",
initializeLogAggregatorData: () => ({}),
createLogAggregator: logAggregatorData => new MemberListLogAggregator(logAggregatorData),
createDomainModel: (dispatch, logAggregatorData) => new MemberListDomainModel(dispatch, logAggregatorData)
}
Use defineStore
to point at where the physical store for the logs. For node.js applications, you can use the default file system provider. If you want to use Eventstore in the browser, you can supply alternative options for how and where the logs are persisted.
The resulting object is the root for either writing straight to the event log or working with instances of the domain model.
const {defineStore} = require("@scriptabuild/eventstore");
let store = defineStore("/foldername");
let model = store.defineModel(modelDefinition);
await model.withReadWriteInstance((instance, readyToCommit) => {
instance.registerNewMember({ name: "arjan einbu", address: {} });
readyToCommit();
});
See the API reference for defineStore
for more information.