Skip to content

Commit

Permalink
simple abstractions for functional event sourcing
Browse files Browse the repository at this point in the history
  • Loading branch information
vvgomes committed Aug 29, 2016
0 parents commit 2344277
Show file tree
Hide file tree
Showing 7 changed files with 191 additions and 0 deletions.
1 change: 1 addition & 0 deletions .babelrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{ "presets": ["es2015"] }
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
node_modules/
1 change: 1 addition & 0 deletions .npmignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
node_modules/
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Upshot

TBD.
31 changes: 31 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
{
"name": "upshot",
"version": "0.0.1",
"description": "functional event sourcing",
"main": "index.js",
"author": "vvgomes",
"license": "ISC",
"repository": {
"type": "git",
"url": "https://github.com/vvgomes/upshot.git"
},
"dependencies": {
"ramda": "^0.19.1",
"data.maybe": "1.2.0",
"data.validation": "^1.2.0"
},
"devDependencies": {
"babel-cli": "^6.6.5",
"babel-preset-es2015": "^6.6.0",
"mocha": "^2.4.5"
},
"babel": {
"presets": [
"es2015"
]
},
"scripts": {
"test": "mocha --compilers js:babel-register --recursive"
}
}

111 changes: 111 additions & 0 deletions test/upshot.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import assert from "assert";
import R from "ramda";
import Validation from "data.validation"
import Upshot from "../upshot";

describe("an event sourcing app", () => {

// just a simple counter
const app = Upshot.createApp({
initialState: { counter: 0 },

eventHandlers: {
counterIncremented: (state, event) => R.evolve({ counter: R.inc }, state),
counterDecremented: (state, event) => R.evolve({ counter: R.dec }, state)
},

commandHandlers: {
incrementCounter: (state, command) => [ { type: "counterIncremented" } ],
decrementCounter: (state, command) => [ { type: "counterDecremented" } ]
}
});

describe("state", () => {
it("is the initial state by default", () => {
assert.deepEqual(
app([]).state(),
{ counter: 0 }
);
});

it("applies previous events", () => {
const events = [
{ type: "counterIncremented" },
{ type: "counterDecremented" },
{ type: "counterIncremented" },
{ type: "counterIncremented" }
];

assert.deepEqual(
app(events).state(),
{ counter: 2 }
);
});

it("applies only known events", () => {
const events = [
{ type: "counterIncremented" },
{ type: "unknown" },
{ type: "counterDecremented" },
{ type: "counterIncremented" }
];

assert.deepEqual(
app(events).state(),
{ counter: 1 }
);
});

it("ignores events without a type", () => {
const events = [
{ type: "counterIncremented" },
{},
{ type: "counterDecremented" },
{ type: "counterIncremented" }
];

assert.deepEqual(
app(events).state(),
{ counter: 1 }
);
});

});

describe("dispatch", () => {
it("produces events after command handled successfully", () => {
const firstCommand = { type: "incrementCounter" };

assert.deepEqual(
app([]).dispatch(firstCommand),
[{ type: "counterIncremented" }]
);

const secondCommand = { type: "decrementCounter" };

assert.deepEqual(
app([]).dispatch(secondCommand),
[{ type: "counterDecremented" }]
);
});

it("produces error when the command is unknown", () => {
const command = { type: "unknown" };

assert.deepEqual(
app([]).dispatch(command),
Validation.Failure(["Cannot handle command of type 'unknown'."])
);
});

it("produces error when the command has no type", () => {
const command = {};

assert.deepEqual(
app([]).dispatch(command),
Validation.Failure(["Cannot handle command of type 'undefined'."])
);
});
});
});

43 changes: 43 additions & 0 deletions upshot.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import R from "ramda";
import Maybe from "data.maybe"
import Validation from "data.validation"

const Success = Validation.Success;
const Failure = Validation.Failure;

const defineHandle = (commandHandlers) =>
(state, command) =>
Maybe
.fromNullable(commandHandlers[command.type])
.map(R.flip(R.apply)([state, command]))
.getOrElse(Failure([`Cannot handle command of type '${command.type}'.`]));

const defineApply = (eventHandlers) =>
(state, event) =>
Maybe
.fromNullable(eventHandlers[event.type])
.map((handler) => handler(state, event))
.getOrElse(state);

const Upshot = {};

Upshot.createApp = (config) => {
const commandHandlers = config.commandHandlers || {};
const eventHandlers = config.eventHandlers || {};
const initialState = config.initialState || {};

const handle = defineHandle(commandHandlers);
const apply = defineApply(eventHandlers);

return (events) => {
const state = R.reduce(apply, initialState, events);

return {
state: () => state,
dispatch: (command) => handle(state, command)
};
};
};

export default Upshot;

0 comments on commit 2344277

Please sign in to comment.