New Case Study:See how Anthropic automated 95% of dependency reviews with Socket.Learn More
Socket
Sign inDemoInstall
Socket

eventsourced

Package Overview
Dependencies
Maintainers
1
Versions
18
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

eventsourced - npm Package Compare versions

Comparing version 1.0.1 to 1.0.2

198

lib/entity/entity.js
const EventEmitter = require('events');
const nlp = require('nlp_compromise');
const Immutable = require('immutable');
const diff = require('immutablediff');
const patch = require('immutablepatch');
class Entity extends EventEmitter {
constructor(events = []) {
super();
this.version = 0;
this.history = Array.isArray(events) ? events : [events];
this.data = {};
this.commands.forEach(command => {
this[command.name] = this.wrap(command);
/**
* These symbols are used as keys for some "private" properties in Entity.
*/
const cqrs = Symbol();
const es = Symbol();
const emitter = Symbol();
/**
* We use a Proxy to trap certain operations so Entity works as expected:
*
* 1. We trap every get operation to check if the operation refers to a command
* and, if so, we route it to the registered CQRS commands.
* 2. We trap set operations to ensure state is kept to par with the entity
* instance's data.
*/
const traps = {
get(target, key) {
const entity = target;
let value = entity[key] || null;
if (entity[cqrs].commands[key]) {
value = entity[cqrs].commands[key];
}
return value;
},
set(target, key, value) {
const entity = target;
entity[es].state = entity[es].state.set(key, value);
entity[key] = entity[es].state.get(key);
return true;
},
};
/**
* EventSourced Entity Class.
*
* This class combines Event Sourcing and CQRS concepts with an event emitter.
* We are doing so through composition at the class level using Symbols to
* hide some of the complexity and keep the instances as clean as possible.
*
* * Event sourcing attributes are referenced through this[es].*
* * CQRS attributes (commands for now) are referenced through this[cqrs].*
* * The Event Emitter is referenced through this[emitter].*
*
* One of the main goals of this class is to create instances that are as clean
* as possible and allow users to set and get attributes as they normally would
* in JavaScript while automatically maintaining state, event history, etc. This
* is why we use Symbols to store internals.
*/
class Entity {
constructor(events = [], options = {}) {
this[es] = {};
this[es].version = 0;
this[es].history = Array.isArray(events) ? events : [events];
this[es].state = Immutable.fromJS({});
this[es].mappings = {};
this[emitter] = new EventEmitter();
this[cqrs] = {};
this[cqrs].commands = {};
Entity.commands(this).forEach(command => {
Entity.command(this, command, this[command]);
});
Object.assign(this[es].mappings, options.mappings);
const proxy = new Proxy(this, traps);
this[es].history.forEach(event => Entity.apply(event, this));
return proxy;
}
get commands() {
const prototype = Object.getPrototypeOf(this);
on(event, listener) {
this[emitter].on(event, listener);
}
emit(event, data) {
this[emitter].emit(event, data);
}
/**
* Get a list of commands defined on the entity.
*
* @param {Entity} entity The entity being acted on.
*/
static commands(entity) {
const prototype = Object.getPrototypeOf(entity);
const commands = Object.getOwnPropertyNames(prototype);
commands.shift();
return commands.map(command => this[command]);
return commands;
}
wrap(fn) {
const name = fn.name;
const event = nlp.verb(name).conjugate().past;
return function wrapped(...args) {
fn.apply(this, args);
this.emit(event, this.data);
/**
* Register a command. Here we take a function and register it under the CQRS
* property in the target using the passed command name. Additionaly, the
* function is wrapped so the following happens:
*
* 1. The state of the entity BEFORE executing the function is held in memory.
* 2. The VALUE returned by the function is held in memory.
* 3. The state AFTER executing the function ir held in memory.
* 4. The before and after states are compared by way of diff.
* 5. If the function has any effect on state AND returns null or undefined,
* we create, apply, record and emit an event.
*
* @param {Entity} target The entity the command is being registered on.
* @param {String} command The name of the commmand being registered.
* @param {Function} fn The function being registered.
*/
static command(target, command, fn) {
const entity = target;
entity[cqrs].commands[command] = function (...args) {
const before = this[es].state;
const value = fn.apply(this, args);
const after = Immutable.fromJS(this);
const changeset = diff(before, after);
if ((!value || value === null) && changeset.size > 0) {
// If executing function returns any value other than undefined or null,
// it will be treated as a query and therefore changes will not be
// recorded into history.
const event = Immutable.fromJS({
name: this[es].mappings[command] || nlp.verb(command).conjugate().past,
version: this[es].version + 1,
changeset,
});
Entity.apply(event, this);
this[es].history.push(event);
this[emitter].emit(event.get('name'), event);
}
return null;
};
}
/**
* Create a snapshot of an entity.
*
* Here we return an immutable diff using an empty object as base and the
* current state of the entity. This essentially gives us a patch that can be
* applied like any other changeset except the expectation is that it will be
* applied to an empty object.
*
* @param {Entity} entity The entity being snapshotted.
*/
static snapshot(entity) {
const event = Immutable.fromJS({
name: 'snapshot',
version: entity[es].version,
changeset: diff(Immutable.fromJS({}), entity[es].state),
});
entity[emitter].emit(event.get('name'), event);
return entity[es].state.toObject();
}
/**
* Apply an event to the entity.
*
* Take an event as expected by this library and apply it to the entity. If it
* is a snapshot event, reset the state to be an empty object.
*
* @param {Object} event The event being applied.
* @param {Entity} target The entity being acted on.
*/
static apply(event, target) {
const entity = target;
let before = entity[es].state;
if (event.get('name') === 'snapshot') {
before = Immutable.fromJS({});
}
entity[es].version = event.get('version');
entity[es].state = patch(before, event.get('changeset'));
}
/**
* Inspect an Entity object.
*
* Because we are using symbols to hide some internals, inspecting an instance
* through common means is not possible. This makes it easy to access
* important information about the entity.
*
* @param {Entity} target The entity being acted on.
*/
static inspect(entity) {
const spec = {};
spec.version = entity[es].version;
spec.history = entity[es].history;
spec.state = entity[es].state;
spec.commands = entity[cqrs].commands;
return spec;
}
}
module.exports = Entity;
const Entity = require('.');
const tap = require('tap');
const fixtures = {
a: {
name: 'Luis',
email: 'l@example.com',
},
b: {
name: 'Daniel',
email: 'd@example.com',
}
};
class TestEntity extends Entity {
post() {}
delete() {}
constructor(events, options) {
super(events, options);
this.name = fixtures.a.name;
this.email = fixtures.a.email;
}
rename(name) {
this.name = name;
}
save() {
this.foo = 'bar';
}
touch() {
}
myQuery() {
return {
type: 'query response',
name: this.name,
email: this.email,
}
}
}
const i = new TestEntity();
// console.log('CREATING INSTACE A');
const a = new TestEntity();
tap.type(i, TestEntity, 'Instance type should be TestEntity');
tap.equals(i.version, 0, 'Instance version should be 0');
tap.type(i.history, Array, 'Instance history should be an Array');
tap.equals(i.history.length, 0, 'Instance history should be empty');
tap.same(i.data, {}, 'Instance data should be {}');
tap.equals(i.commands.length, 2, 'Instance should have 2 commands');
tap.equals(Entity.inspect(a).version, 0, 'Instance version should be 0');
tap.equals(a.name, fixtures.a.name, `Instance name should be ${fixtures.a.name}`);
tap.equals(i.post(), null, 'Instance.post() should return null');
i.on('posted', data => {
tap.same(data, {}, 'Emitted data should be {}');
});
i.post();
tap.equals(i.delete(), null, 'Instance.delete() should return null');
i.on('deleted', data => {
tap.same(data, {}, 'Emitted data should be {}');
});
i.delete();
a.rename(fixtures.b.name);
tap.equals(Entity.inspect(a).version, 1, 'Instance version should be 1');
tap.equals(a.name, fixtures.b.name, `Instance name should be ${fixtures.b.name}`);
a.save();
tap.equals(Entity.inspect(a).version, 2, 'Instance version should be 2');
tap.equals(Entity.inspect(a).history.length, 2, 'Instance history should contain 2 entries');
a.touch();
tap.equals(Entity.inspect(a).version, 2, 'Instance version should be 2');
tap.equals(Entity.inspect(a).history.length, 2, 'Instance history should contain 2 entries');
tap.same(Entity.snapshot(a), {
name: fixtures.b.name,
email: fixtures.a.email,
foo: 'bar',
}, 'Instance snapshot should...');
// tap.test('Instance should emit "saved" event', t => {
// i.on('saved', () => {
// t.equals(Entity.version(i), 1, 'Instance version should be 1');
// t.equals(Entity.history(i).length, 1, 'Instance history should have one event');
// // t.same(Entity.snapshot(i), {
// // version: 1,
// // changeset: i,
// // }, 'Instance snapshot should...');
// t.end();
// });
// i.save();
// });
//
// tap.test('Instance should emit "renamed" event', t => {
// i.on('renamed', () => {
// t.equals(Entity.version(i), 2, 'Instance version should be 2');
// t.equals(Entity.history(i).length, 2, 'Instance history should have two events');
// // t.same(Entity.snapshot(i), {
// // version: 2,
// // changeset: i,
// // }, 'Instance snapshot should...');
// t.end();
// });
// i.rename('Peter');
// });
//
// tap.test('Instance should emit "touched" event', t => {
// i.on('touched', () => {
// t.equals(Entity.version(i), 3, 'Instance version should be 3');
// t.equals(Entity.history(i).length, 3, 'Instance history should have three events');
// // t.same(Entity.snapshot(i), {
// // version: 3,
// // changeset: i,
// // }, 'Instance snapshot should...');
// t.end();
// });
// i.touch();
// });
//
// tap.test('Registering a command', t => {
// Entity.command(i, 'fix', function cmd() {
// this.fixed = true;
// });
// i.on('fixed', () => {
// t.equals(Entity.version(i), 4, 'Instance version should be 4');
// t.equals(Entity.history(i).length, 4, 'Instance history should have four events');
// t.end();
// });
// i.fix();
// });
//
// tap.test('Recreating an entity from a snapshot', t => {
// const b = new TestEntity(Entity.snapshot(i));
// console.log(Entity.inspect(b));
// t.end();
// });
// console.log(Entity.inspect(i));
// console.log(i.set('new@email.com'));
// console.log(Entity.commands(i));
// console.log(Entity.version(i));
// console.log(Entity.history(i));
// console.log(Entity.data(i));
// console.log(Entity.inspect(i));
// const i = new Entity(new TestEntity());
//
// console.log(i.name);
// console.log(i.post());
//
//
// const b = new Entity(new TestEntity());
//
// console.log(b.name);
// console.log(b.post());
// setTimeout(() => {
// console.log(b.post());
// }, 500);
//
// Entity.getHistoryOf(b);
// Entity.getVersionOf(b);
// Entity.getStateOf(b);
// tap.type(i, TestEntity, 'Instance type should be TestEntity');
// tap.equals(i.version, 0, 'Instance version should be 0');
// tap.type(i.history, Array, 'Instance history should be an Array');
// tap.equals(i.history.length, 0, 'Instance history should be empty');
// tap.ok(i.data.equals(Map({})), 'Instance data should be {}');
// tap.equals(i.commands.length, 4, 'Instance should have 2 commands');
// tap.equals(i.post(), null, 'Instance.post() should return null');
// i.on('posted', data => {
// tap.equals(data, i.data, 'Emitted data should be {}');
// });
// i.post();
// tap.equals(i.delete(), null, 'Instance.delete() should return null');
// i.on('deleted', data => {
// tap.same(data, {}, 'Emitted data should be {}');
// });
// i.delete();
// console.log(i.events);
// console.log(nlp.verb('set').conjugate().past);
// console.log(nlp.verb('change').conjugate().past);
// console.log(nlp.verb('changeEmail').conjugate().past);
//
// console.log(nlp.verb('set').conjugate().infinitive);
// console.log(nlp.verb('changed').conjugate().infinitive);
// console.log(nlp.verb('changeEmailed').conjugate().infinitive);

4

package.json
{
"name": "eventsourced",
"version": "1.0.1",
"version": "1.0.2",
"description": "An Event Sourcing library for Node",

@@ -15,3 +15,3 @@ "main": "index.js",

"test": "tap -R spec lib/**/*.spec.js",
"test:watch": "nodemon -q --exec 'npm t'"
"start": "nodemon -q --exec 'npm t'"
},

@@ -18,0 +18,0 @@ "keywords": [

@@ -5,26 +5,4 @@ # Event Sourced

Work in progress. For now, see [lib/entity/entity.spec.js](lib/entity/entity.spec.js) to get an idea of what it does.
Combining Event Sourcing and CQRS concepts in one Entity class for node using ES6 Symbols, Proxies, Immutable and Event Emitter. One of my main goals with the Entity class is to create instances that are as clean as possible and allow users to set and get attributes as they normally would in JavaScript while automatically maintaining state, event history, etc.
With this small library I'm aiming to provide an easy way to implement event sourced entities in Node. My goal is to be able to do something like this:
```javascript
class MyEntity extends Entity {
save() {}
delete() {}
}
// Get some events from some sort of store...
const events = [
{ event: 'saved', patch: { op: "save", path: "/name", value: "Luis" } },
];
// Create an instance with those events... Or none.
const instance = new MyEntity(events); // Instance is created applying events.
assert(instance.name === "Luis"); // State should reflect the state up to the last event.
instance.nickname = "luisgo"; // Instance can be manipulated as usual
const instance.save(); // emit 'saved' event with immutable event data.
const instance.delete(); // emit 'deleted' event with immutable data.
```
This is very much a work in progress and not ready for use. For now, see [lib/entity/entity.spec.js](lib/entity/entity.spec.js) to get an idea of what it does.
SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc