├── .gitignore ├── package.json ├── src ├── bootstrapper.js ├── messageBus.js ├── errors.js ├── aggregateRoot.js ├── commandHandlers.js ├── reportDatabase.js ├── eventStore.js ├── application.js ├── inventoryItem.js └── reportAggregators.js ├── LICENSE └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | node_modules/ 3 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "node-m-r", 3 | "description": "Greg Young's simple CQRS example written using Node.js (https://github.com/gregoryyoung/m-r).", 4 | "version": "0.1.0", 5 | "author": "Jan Van Ryswyck (http://elegantcode.com)", 6 | "engines": { 7 | "node": "0.10.x" 8 | }, 9 | "dependencies": { 10 | "eventemitter2": "~0.4.x", 11 | "lodash": "~2.x.x", 12 | "node-uuid": "~1.x.x", 13 | "either.js": "~0.0.1" 14 | } 15 | } -------------------------------------------------------------------------------- /src/bootstrapper.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var reporting = require('./reportAggregators'), 4 | messageBus = require('./messageBus'); 5 | 6 | exports.bootstrap = function() { 7 | var inventoryReportAggregator = new reporting.InventoryReportAggregator(); 8 | messageBus.registerEventHandler(inventoryReportAggregator); 9 | 10 | var inventoryDetailsReportAggregator = new reporting.InventoryDetailsReportAggregator(); 11 | messageBus.registerEventHandler(inventoryDetailsReportAggregator); 12 | }; -------------------------------------------------------------------------------- /src/messageBus.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var messageBus = (function() { 4 | var _this = {}, 5 | _eventHandlers = []; 6 | 7 | _this.registerEventHandler = function(eventHandler) { 8 | _eventHandlers.push(eventHandler); 9 | }; 10 | 11 | _this.publish = function(domainEvent) { 12 | _eventHandlers.forEach(function(eventHandler) { 13 | process.nextTick(function() { 14 | eventHandler.write(domainEvent); 15 | }); 16 | }); 17 | }; 18 | 19 | return _this; 20 | })(); 21 | 22 | module.exports = messageBus; -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 Jan Van Ryswyck 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # node-m-r 2 | 3 | Greg Young's simple CQRS example written using Node.js. 4 | [https://github.com/gregoryyoung/m-r](https://github.com/gregoryyoung/m-r). 5 | 6 | ## Build 7 | 8 | ``` bash 9 | $ npm install 10 | $ node src/application.js 11 | ``` 12 | 13 | ## License 14 | 15 | The MIT License (MIT) 16 | 17 | Copyright (c) 2013 Jan Van Ryswyck 18 | 19 | Permission is hereby granted, free of charge, to any person obtaining a copy of 20 | this software and associated documentation files (the "Software"), to deal in 21 | the Software without restriction, including without limitation the rights to 22 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 23 | the Software, and to permit persons to whom the Software is furnished to do so, 24 | subject to the following conditions: 25 | 26 | The above copyright notice and this permission notice shall be included in all 27 | copies or substantial portions of the Software. 28 | 29 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 30 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 31 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 32 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 33 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 34 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /src/errors.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var util = require('util'); 4 | 5 | // 6 | // InvalidOperationError 7 | // 8 | var InvalidOperationError = exports.InvalidOperationError = function(message, error) { 9 | this.error = error; 10 | this.name = 'InvalidOperationError'; 11 | 12 | Error.call(this, message); 13 | Error.captureStackTrace(this, arguments.callee); 14 | }; 15 | 16 | util.inherits(InvalidOperationError, Error); 17 | 18 | 19 | // 20 | // ConcurrencyError 21 | // 22 | var ConcurrencyViolationError = exports.ConcurrencyError = function(message, error) { 23 | this.error = error; 24 | this.name = 'ConcurrencyViolationError'; 25 | 26 | Error.call(this, message); 27 | Error.captureStackTrace(this, arguments.callee); 28 | }; 29 | 30 | util.inherits(ConcurrencyViolationError, Error); 31 | 32 | 33 | // 34 | // InvalidDataAreaError 35 | // 36 | var InvalidDataAreaError = exports.InvalidDataAreaError = function(message, error) { 37 | this.error = error; 38 | this.name = 'InvalidDataAreaError'; 39 | 40 | Error.call(this, message); 41 | Error.captureStackTrace(this, arguments.callee); 42 | }; 43 | 44 | util.inherits(InvalidDataAreaError, Error); 45 | 46 | 47 | // 48 | // ReportNotFoundError 49 | // 50 | var ReportNotFoundError = exports.ReportNotFoundError = function(message, error) { 51 | this.error = error; 52 | this.name = 'ReportNotFoundError'; 53 | 54 | Error.call(this, message); 55 | Error.captureStackTrace(this, arguments.callee); 56 | }; 57 | 58 | util.inherits(ReportNotFoundError, Error); -------------------------------------------------------------------------------- /src/aggregateRoot.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var EventEmitter = require('eventemitter2').EventEmitter2, 4 | util = require('util'), 5 | stream = require('stream'), 6 | uuidGenerator = require('node-uuid'); 7 | 8 | module.exports = AggregateRoot; 9 | 10 | function AggregateRoot(id) { 11 | this._id = id; 12 | this._version = this._eventVersion = 0; 13 | this._transientEvents = []; 14 | 15 | this._eventEmitter = new EventEmitter(); 16 | stream.Writable.call(this, { objectMode: true }); 17 | }; 18 | 19 | util.inherits(AggregateRoot, stream.Writable); 20 | 21 | AggregateRoot.prototype.apply = function(eventName, domainEvent) { 22 | this._eventVersion += 1; 23 | enhanceDomainEvent(this, eventName, this._eventVersion, domainEvent); 24 | 25 | this._transientEvents.push(domainEvent); 26 | this._eventEmitter.emit(eventName, domainEvent); 27 | }; 28 | 29 | AggregateRoot.prototype.getTransientEvents = function() { 30 | return this._transientEvents; 31 | }; 32 | 33 | AggregateRoot.prototype.getId = function() { 34 | return this._id; 35 | }; 36 | 37 | AggregateRoot.prototype.getVersion = function() { 38 | return this._version; 39 | }; 40 | 41 | AggregateRoot.prototype.onEvent = function(type, listener) { 42 | return this._eventEmitter.on(type, listener); 43 | }; 44 | 45 | AggregateRoot.prototype._write = function(domainEvent, encoding, next) { 46 | this._eventEmitter.emit(domainEvent.eventName, domainEvent); 47 | 48 | this._eventVersion += 1; 49 | this._version += 1; 50 | next(); 51 | }; 52 | 53 | function enhanceDomainEvent(aggregateRoot, eventName, eventVersion, domainEvent) { 54 | domainEvent.aggregateRootId = aggregateRoot._id; 55 | domainEvent.eventId = uuidGenerator.v1(); 56 | domainEvent.eventName = eventName; 57 | domainEvent.eventVersion = eventVersion; 58 | } -------------------------------------------------------------------------------- /src/commandHandlers.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var inventoryItemDomain = require('./inventoryItem'), 4 | MessageBus = require('./messageBus'); 5 | 6 | var repository = new inventoryItemDomain.Repository(); 7 | var DEFAULT_NUMBER_OF_ITEMS_IN_INVENTORY = 15; 8 | 9 | exports.createInventoryItem = function(command, callback) { 10 | var inventoryItem = inventoryItemDomain.create(command.inventoryItemId, command.name); 11 | inventoryItem.checkIn(DEFAULT_NUMBER_OF_ITEMS_IN_INVENTORY); 12 | 13 | repository.save(inventoryItem, callback); 14 | }; 15 | 16 | exports.renameInventoryItem = function(command, callback) { 17 | repository.get(command.inventoryItemId, function(error, inventoryItem) { 18 | if(error) { 19 | callback(error); 20 | return; 21 | } 22 | 23 | inventoryItem.rename(command.name); 24 | repository.save(inventoryItem, callback); 25 | }); 26 | }; 27 | 28 | exports.checkinItemsInToInventory = function(command, callback) { 29 | repository.get(command.inventoryItemId, function(error, inventoryItem) { 30 | if(error) { 31 | callback(error); 32 | return; 33 | } 34 | 35 | inventoryItem.checkIn(command.numberOfItems); 36 | repository.save(inventoryItem, callback); 37 | }); 38 | }; 39 | 40 | exports.checkoutItemsFromInventory = function(command, callback) { 41 | repository.get(command.inventoryItemId, function(error, inventoryItem) { 42 | if(error) { 43 | callback(error); 44 | return; 45 | } 46 | 47 | try { 48 | inventoryItem.checkOut(command.numberOfItems); 49 | } 50 | catch(error) { 51 | callback(error); 52 | return; 53 | } 54 | 55 | repository.save(inventoryItem, callback); 56 | }); 57 | }; 58 | 59 | exports.deactivateInventoryItem = function(command, callback) { 60 | repository.get(command.inventoryItemId, function(error, inventoryItem) { 61 | if(error) { 62 | callback(error); 63 | return; 64 | } 65 | 66 | try { 67 | inventoryItem.deactivate(); 68 | } 69 | catch(error) { 70 | callback(error); 71 | return; 72 | } 73 | 74 | repository.save(inventoryItem, callback); 75 | }); 76 | }; -------------------------------------------------------------------------------- /src/reportDatabase.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var either = require('either.js'), 4 | _ = require('lodash'), 5 | InvalidDataAreaError = require('./errors').InvalidDataAreaError; 6 | 7 | var reportDatabase = (function() { 8 | var _this = {}; 9 | 10 | var _dataAreas = { 11 | InventoryReports: [], 12 | InventoryDetailsReports: [] 13 | }; 14 | 15 | _this.createDump = function() { 16 | return _dataAreas; 17 | }; 18 | 19 | _this.getReport = function(dataArea, id, callback) { 20 | simulateAsynchronousIO(function() { 21 | getReportsCollectionFor(dataArea).fold( 22 | function left(error) { 23 | callback(error); 24 | }, 25 | function right(reportsCollection) { 26 | var requestedReport = _.find(reportsCollection, function(report) { 27 | return report.id === id; 28 | }); 29 | 30 | callback(null, requestedReport); 31 | } 32 | ); 33 | }); 34 | }; 35 | 36 | _this.insertReport = function(dataArea, inventoryReport, callback) { 37 | simulateAsynchronousIO(function() { 38 | getReportsCollectionFor(dataArea).fold( 39 | function left(error) { 40 | callback(error); 41 | }, 42 | function right(reportsCollection) { 43 | reportsCollection.push(inventoryReport); 44 | callback(); 45 | } 46 | ); 47 | }); 48 | }; 49 | 50 | _this.removeReport = function(dataArea, id, callback) { 51 | simulateAsynchronousIO(function() { 52 | getReportsCollectionFor(dataArea).fold( 53 | function left(error) { 54 | callback(error); 55 | }, 56 | function right(reportsCollection) { 57 | _.remove(reportsCollection, function(report) { 58 | return report.id === id; 59 | }); 60 | 61 | callback(); 62 | } 63 | ); 64 | }); 65 | }; 66 | 67 | function simulateAsynchronousIO(asynchronousAction) { 68 | process.nextTick(asynchronousAction); 69 | } 70 | 71 | function getReportsCollectionFor(dataArea) { 72 | var reportsCollection = _dataAreas[dataArea]; 73 | 74 | if(reportsCollection) 75 | return either.right(reportsCollection); 76 | else 77 | return either.left(new InvalidDataAreaError('The specified data area is unknown.')); 78 | } 79 | 80 | return _this; 81 | })(); 82 | 83 | module.exports = reportDatabase; -------------------------------------------------------------------------------- /src/eventStore.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var stream = require('stream'), 4 | _ = require('lodash'), 5 | ConcurrencyViolationError = require('./errors').ConcurrencyViolationError; 6 | 7 | var eventStore = (function() { 8 | var _this = {}, 9 | _store = []; 10 | 11 | _this.createDump = function() { 12 | return _store; 13 | }; 14 | 15 | _this.getAllEventsFor = function(aggregateRootId, callback) { 16 | findStoredDomainEvents(aggregateRootId, function(error, storedDocument) { 17 | var eventStream; 18 | 19 | if(error) 20 | return callback(error); 21 | 22 | if(!storedDocument) 23 | return callback(); 24 | 25 | eventStream = new stream.PassThrough({ objectMode: true }); 26 | 27 | storedDocument.events.forEach(function(domainEvent) { 28 | eventStream.write(domainEvent); 29 | }); 30 | 31 | eventStream.end(); 32 | callback(null, eventStream); 33 | }); 34 | }; 35 | 36 | _this.save = function(domainEvents, aggregateRootId, expectedAggregateRootVersion, callback) { 37 | findStoredDomainEvents(aggregateRootId, function(error, storedDocument) { 38 | var storedDocument, concurrencyViolation; 39 | 40 | if(error) 41 | return callback(error); 42 | 43 | if(!storedDocument) { 44 | storedDocument = { 45 | id: aggregateRootId, 46 | events: domainEvents 47 | }; 48 | 49 | _store.push(storedDocument); 50 | return callback(); 51 | } 52 | 53 | if(_.last(storedDocument.events).eventVersion !== expectedAggregateRootVersion) { 54 | concurrencyViolation = new ConcurrencyViolationError('An operation has been performed on an aggregate root that is out of date.'); 55 | return callback(concurrencyViolation); 56 | } 57 | 58 | domainEvents.forEach(function(domainEvent) { 59 | storedDocument.events.push(domainEvent); 60 | }); 61 | 62 | callback(); 63 | }); 64 | }; 65 | 66 | function findStoredDomainEvents(aggregateRootId, callback) { 67 | simulateAsynchronousIO(function() { 68 | var storedDocument = _.find(_store, function(document) { 69 | return document.id === aggregateRootId; 70 | }); 71 | 72 | callback(null, storedDocument); 73 | }); 74 | } 75 | 76 | function simulateAsynchronousIO(asynchronousAction) { 77 | process.nextTick(asynchronousAction); 78 | } 79 | 80 | return _this; 81 | })(); 82 | 83 | module.exports = eventStore; -------------------------------------------------------------------------------- /src/application.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var uuidGenerator = require('node-uuid'), 4 | _ = require('lodash'), 5 | eventStore = require('./eventStore'), 6 | reportDatabase = require('./reportDatabase'), 7 | commandHandlers = require('./commandHandlers'); 8 | 9 | require('./bootstrapper').bootstrap(); 10 | 11 | var inventoryItemId = uuidGenerator.v1(); 12 | 13 | (function step01() { 14 | console.log('======================================================'); 15 | console.log('Run the CreateInventoryItem command handler'); 16 | console.log('======================================================'); 17 | 18 | var command = { 19 | inventoryItemId: inventoryItemId, 20 | name: 'Something' 21 | }; 22 | 23 | commandHandlers.createInventoryItem(command, function(error) { 24 | if(error) { 25 | console.log(error); 26 | return; 27 | } 28 | 29 | printCurrentStateOfTheApplication(); 30 | setTimeout(function() { step02(); }, 5000); 31 | }); 32 | })(); 33 | 34 | function step02() { 35 | console.log('======================================================'); 36 | console.log('Run the RenameInventoryItem command handler'); 37 | console.log('======================================================'); 38 | 39 | var command = { 40 | inventoryItemId: inventoryItemId, 41 | name: 'Something entirely different' 42 | }; 43 | 44 | commandHandlers.renameInventoryItem(command, function(error) { 45 | if(error) { 46 | console.log(error); 47 | return; 48 | } 49 | 50 | printCurrentStateOfTheApplication(); 51 | setTimeout(function() { step03(); }, 5000); 52 | }); 53 | } 54 | 55 | function step03() { 56 | console.log('======================================================'); 57 | console.log('Run the CheckoutItemsFromInventory command handler'); 58 | console.log('======================================================'); 59 | 60 | var command = { 61 | inventoryItemId: inventoryItemId, 62 | numberOfItems: 7 63 | }; 64 | 65 | commandHandlers.checkoutItemsFromInventory(command, function(error) { 66 | if(error) { 67 | console.log(error); 68 | return; 69 | } 70 | 71 | printCurrentStateOfTheApplication(); 72 | setTimeout(function() { step04(); }, 5000); 73 | }); 74 | } 75 | 76 | function step04() { 77 | console.log('======================================================'); 78 | console.log('Run the DeactivateInventoryItem command handler'); 79 | console.log('======================================================'); 80 | 81 | var command = { 82 | inventoryItemId: inventoryItemId 83 | }; 84 | 85 | commandHandlers.deactivateInventoryItem(command, function(error) { 86 | if(error) { 87 | console.log(error); 88 | return; 89 | } 90 | 91 | printCurrentStateOfTheApplication(); 92 | }); 93 | } 94 | 95 | function printCurrentStateOfTheApplication() { 96 | printEventStoreContent(); 97 | 98 | // Give the report database some time to catch up 99 | setTimeout(function() { 100 | printReportDatabaseContent(); 101 | }, 2000); 102 | } 103 | 104 | function printEventStoreContent() { 105 | console.log('******************************************************'); 106 | console.log('Event store'); 107 | console.log('******************************************************'); 108 | _.forEach(eventStore.createDump(), function(document) { console.log(document.events); }); 109 | } 110 | 111 | function printReportDatabaseContent() { 112 | console.log('******************************************************'); 113 | console.log('Report database'); 114 | console.log('******************************************************'); 115 | console.log(reportDatabase.createDump()); 116 | } -------------------------------------------------------------------------------- /src/inventoryItem.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var AggregateRoot = require('./aggregateRoot'), 4 | InvalidOperationError = require('./errors').InvalidOperationError, 5 | util = require('util'), 6 | eventStore = require('./eventStore'), 7 | messageBus = require('./messageBus'); 8 | 9 | exports.create = function create(id, name) { 10 | return new InventoryItem(id, name); 11 | }; 12 | 13 | exports.Repository = InventoryItemRepository; 14 | 15 | // 16 | // InventoryItem 17 | // 18 | function InventoryItem(id, name) { 19 | var _this = this; 20 | 21 | this._activated = true; 22 | this._name = ''; 23 | this._number = 0; 24 | 25 | AggregateRoot.call(this, id); 26 | subscribeToDomainEvents(this); 27 | 28 | if(name) { 29 | this.apply('InventoryItemCreated', { 30 | name: name 31 | }); 32 | } 33 | }; 34 | 35 | util.inherits(InventoryItem, AggregateRoot); 36 | 37 | InventoryItem.prototype.checkIn = function(numberOfItems) { 38 | this.apply('ItemsCheckedInToInventory', { 39 | numberOfItems: numberOfItems 40 | }); 41 | }; 42 | 43 | InventoryItem.prototype.checkOut = function(numberOfItems) { 44 | if((this._number - numberOfItems) < 0) { 45 | var errorMesage = util.format('The inventory needs to replenished in order to checkout %d items.', numberOfItems); 46 | throw new InvalidOperationError(errorMesage); 47 | } 48 | 49 | this.apply('ItemsCheckedOutFromInventory', { 50 | numberOfItems: numberOfItems 51 | }); 52 | }; 53 | 54 | InventoryItem.prototype.deactivate = function() { 55 | if(!this._activated) 56 | throw new InvalidOperationError('This inventory item has already been deactivated.'); 57 | 58 | this.apply('InventoryItemDeactivated', {}); 59 | }; 60 | 61 | InventoryItem.prototype.rename = function(name) { 62 | this.apply('InventoryItemRenamed', { 63 | name: name 64 | }); 65 | }; 66 | 67 | function subscribeToDomainEvents(inventoryItem) { 68 | var _this = inventoryItem; 69 | 70 | inventoryItem.onEvent('InventoryItemCreated', function(inventoryItemCreated) { 71 | _this._activated = true; 72 | _this._name = inventoryItemCreated.name; 73 | }); 74 | 75 | inventoryItem.onEvent('InventoryItemRenamed', function(inventoryItemRenamed) { 76 | _this._name = inventoryItemRenamed.name; 77 | }); 78 | 79 | inventoryItem.onEvent('ItemsCheckedInToInventory', function(itemsCheckedInToInventory) { 80 | _this._number += itemsCheckedInToInventory.numberOfItems; 81 | }); 82 | 83 | inventoryItem.onEvent('ItemsCheckedOutFromInventory', function(itemsCheckedOutFromInventory) { 84 | _this._number -= itemsCheckedOutFromInventory.numberOfItems; 85 | }); 86 | 87 | inventoryItem.onEvent('InventoryItemDeactivated', function(inventoryItemDeactivated) { 88 | _this._activated = false; 89 | }); 90 | } 91 | 92 | 93 | // 94 | // InventoryItemRepository 95 | // 96 | function InventoryItemRepository() { 97 | }; 98 | 99 | InventoryItemRepository.prototype.save = function(inventoryItem, callback) { 100 | var transientEvents = inventoryItem.getTransientEvents(); 101 | 102 | eventStore.save(transientEvents, inventoryItem.getId(), inventoryItem.getVersion(), function(error) { 103 | if(error) 104 | return callback(error); 105 | 106 | transientEvents.forEach(function(domainEvent) { 107 | messageBus.publish(domainEvent); 108 | }); 109 | 110 | callback(); 111 | }); 112 | } 113 | 114 | InventoryItemRepository.prototype.get = function(inventoryItemId, callback) { 115 | eventStore.getAllEventsFor(inventoryItemId, function(error, eventStream) { 116 | if(error) 117 | return callback(error); 118 | 119 | if(!eventStream) 120 | return callback(); 121 | 122 | var inventoryItem = new InventoryItem(inventoryItemId); 123 | 124 | eventStream.pipe(inventoryItem) 125 | .on('error', function(error) { 126 | callback(error); 127 | }) 128 | .on('finish', function() { 129 | eventStream.unpipe(); 130 | callback(null, inventoryItem); 131 | }); 132 | }); 133 | }; -------------------------------------------------------------------------------- /src/reportAggregators.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var stream = require('stream'), 4 | util = require('util'), 5 | reportDatabase = require('./reportDatabase'), 6 | ReportNotFoundError = require('./errors.js'); 7 | 8 | exports.InventoryReportAggregator = InventoryReportAggregator; 9 | exports.InventoryDetailsReportAggregator = InventoryDetailsReportAggregator; 10 | 11 | // 12 | // ReportAggregator 13 | // 14 | function ReportAggregator() { 15 | stream.Writable.call(this, { objectMode: true }); 16 | }; 17 | 18 | util.inherits(ReportAggregator, stream.Writable); 19 | 20 | ReportAggregator.prototype._write = function(domainEvent, encoding, next) { 21 | var eventHandlerName = 'handle' + domainEvent.eventName; 22 | var eventHandler = this[eventHandlerName] || dummyEventHandler; 23 | 24 | eventHandler(domainEvent, function(error) { 25 | if(error) { 26 | console.log(error); 27 | return; 28 | } 29 | 30 | next(); 31 | }); 32 | }; 33 | 34 | function dummyEventHandler(domainEvent, callback) { 35 | process.nextTick(callback); 36 | }; 37 | 38 | 39 | // 40 | // InventoryReportAggregator 41 | // 42 | var INVENTORY_REPORTS = 'InventoryReports'; 43 | 44 | function InventoryReportAggregator() { 45 | ReportAggregator.call(this, { objectMode: true }); 46 | }; 47 | 48 | util.inherits(InventoryReportAggregator, ReportAggregator); 49 | 50 | InventoryReportAggregator.prototype.handleInventoryItemCreated = function(message, callback) { 51 | var inventoryReport = { 52 | id: message.aggregateRootId, 53 | name: message.name 54 | }; 55 | 56 | reportDatabase.insertReport(INVENTORY_REPORTS, inventoryReport, callback); 57 | }; 58 | 59 | InventoryReportAggregator.prototype.handleInventoryItemRenamed = function(message, callback) { 60 | reportDatabase.getReport(INVENTORY_REPORTS, message.aggregateRootId, 61 | function(error, inventoryReport) { 62 | if(error) 63 | return callback(error); 64 | 65 | if(!inventoryReport) 66 | return reportNotFound(message.aggregateRootId, callback); 67 | 68 | inventoryReport.name = message.name; 69 | callback(); 70 | } 71 | ); 72 | }; 73 | 74 | InventoryReportAggregator.prototype.handleInventoryItemDeactivated = function(message, callback) { 75 | reportDatabase.removeReport(INVENTORY_REPORTS, message.aggregateRootId, callback); 76 | }; 77 | 78 | 79 | // 80 | // InventoryDetailsReportAggregator 81 | // 82 | var INVENTORY_DETAILS_REPORTS = 'InventoryDetailsReports'; 83 | 84 | function InventoryDetailsReportAggregator() { 85 | ReportAggregator.call(this, { objectMode: true }); 86 | }; 87 | 88 | util.inherits(InventoryDetailsReportAggregator, ReportAggregator); 89 | 90 | InventoryDetailsReportAggregator.prototype.handleInventoryItemCreated = function(message, callback) { 91 | var inventoryDetailsReport = { 92 | currentNumber: 0, 93 | id: message.aggregateRootId, 94 | name: message.name 95 | }; 96 | 97 | reportDatabase.insertReport(INVENTORY_DETAILS_REPORTS, inventoryDetailsReport, callback); 98 | }; 99 | 100 | InventoryDetailsReportAggregator.prototype.handleInventoryItemRenamed = function(message, callback) { 101 | reportDatabase.getReport(INVENTORY_DETAILS_REPORTS, message.aggregateRootId, 102 | function(error, inventoryReport) { 103 | if(error) 104 | return callback(error); 105 | 106 | if(!inventoryReport) 107 | return reportNotFound(message.aggregateRootId, callback); 108 | 109 | inventoryReport.name = message.name; 110 | callback(); 111 | } 112 | ); 113 | }; 114 | 115 | InventoryDetailsReportAggregator.prototype.handleItemsCheckedInToInventory = function(message, callback) { 116 | reportDatabase.getReport(INVENTORY_DETAILS_REPORTS, message.aggregateRootId, 117 | function(error, inventoryReport) { 118 | if(error) 119 | return callback(error); 120 | 121 | if(!inventoryReport) 122 | return reportNotFound(message.aggregateRootId, callback); 123 | 124 | inventoryReport.currentNumber += message.numberOfItems; 125 | callback(); 126 | } 127 | ); 128 | }; 129 | 130 | InventoryDetailsReportAggregator.prototype.handleItemsCheckedOutFromInventory = function(message, callback) { 131 | reportDatabase.getReport(INVENTORY_DETAILS_REPORTS, message.aggregateRootId, 132 | function(error, inventoryReport) { 133 | if(error) 134 | return callback(error); 135 | 136 | if(!inventoryReport) 137 | return reportNotFound(message.aggregateRootId, callback); 138 | 139 | inventoryReport.currentNumber -= message.numberOfItems; 140 | callback(); 141 | } 142 | ); 143 | }; 144 | 145 | InventoryDetailsReportAggregator.prototype.handleInventoryItemDeactivated = function(message, callback) { 146 | reportDatabase.removeReport(INVENTORY_DETAILS_REPORTS, message.aggregateRootId, callback); 147 | }; 148 | 149 | 150 | // 151 | // Helper functions 152 | // 153 | function reportNotFound(aggregateRootId, callback) { 154 | var errorMesage = util.format('The report with identifier "%d" could not be found in the data store.', aggregateRootId); 155 | callback(new ReportNotFoundError(errorMessage)); 156 | } --------------------------------------------------------------------------------