├── .nvmrc ├── .gitignore ├── test ├── helper.js └── mocha.opts ├── .prettierrc ├── .eslintrc ├── lib ├── query │ ├── index.js │ ├── queryBus.js │ ├── projectionUpdater.js │ └── projectionUpdater.spec.js ├── tools │ ├── eventBus.js │ ├── map.js │ ├── index.js │ ├── uuid.js │ ├── validate.js │ ├── object.js │ ├── stream.js │ ├── object.spec.js │ ├── sanitize.js │ ├── stream.spec.js │ └── sanitize.spec.js ├── index.js ├── test │ ├── index.js │ ├── stubMessageBus.js │ ├── fakeRequest.js │ ├── fakeResponse.js │ ├── memoryEventStore.js │ └── memoryEventStore.spec.js ├── types │ ├── repositoryContract.js │ ├── verbHandlerContract.js │ ├── aggregateRootContract.js │ ├── messageHandlerContract.js │ ├── projectionUpdaterContract.js │ ├── eventStoreContract.js │ └── index.js ├── web │ ├── objectToHtml.js │ ├── headerParsing.js │ ├── index.js │ ├── middlewares │ │ ├── latencySimulatorMiddleware.js │ │ ├── prettyMiddleware.js │ │ ├── limitFieldsMiddleware.js │ │ ├── unhandledErrorMiddleware.js │ │ └── unhandledErrorMiddleware.spec.js │ └── headerParsing.spec.js ├── errors │ ├── web │ │ ├── clientError.js │ │ ├── forbiddenError.js │ │ ├── serverError.js │ │ ├── resourceNotFoundError.js │ │ ├── webError.js │ │ ├── unauthorizedError.js │ │ ├── webError.spec.js │ │ ├── forbiddenError.spec.js │ │ ├── unauthorizedError.spec.js │ │ ├── resourceNotFoundError.spec.js │ │ ├── serverError.spec.js │ │ └── clientError.spec.js │ ├── generic │ │ ├── technicalError.js │ │ ├── functionalError.js │ │ ├── customError.js │ │ ├── customError.spec.js │ │ ├── technicalError.spec.js │ │ └── functionalError.spec.js │ ├── validationError.js │ ├── authorizationError.js │ ├── entityNotFoundError.js │ ├── queriedObjectNotFoundError.js │ ├── entityNotFoundError.spec.js │ ├── queriedObjectNotFoundError.spec.js │ ├── validationError.spec.js │ ├── authorizationError.spec.js │ └── index.js └── domain │ ├── commandBus.js │ ├── index.js │ ├── test │ ├── catEventHandlers.js │ ├── cat.js │ └── catRepository.js │ ├── commandDecorators │ ├── broadcastEventsDecorator.js │ ├── addEventsToStoreDecorator.js │ ├── addEventsToStoreDecorator.spec.js │ └── broadcastEventsDecorator.spec.js │ ├── aggregateRoot.js │ ├── repository.js │ ├── aggregateRoot.spec.js │ └── repository.spec.js ├── .travis.yml ├── .editorconfig ├── README.md ├── LICENSE └── package.json /.nvmrc: -------------------------------------------------------------------------------- 1 | 6.11.5 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.log 3 | -------------------------------------------------------------------------------- /test/helper.js: -------------------------------------------------------------------------------- 1 | require('chai').should(); 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true 3 | } 4 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "arpinum/configurations/backend" 3 | } 4 | -------------------------------------------------------------------------------- /test/mocha.opts: -------------------------------------------------------------------------------- 1 | --require ./test/helper.js 2 | --colors 3 | --reporter dot 4 | --recursive 5 | -------------------------------------------------------------------------------- /lib/query/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | ProjectionUpdater: require('./projectionUpdater'), 3 | QueryBus: require('./queryBus') 4 | }; 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "6.11" 4 | notifications: 5 | email: 6 | on_success: never 7 | on_failure: always 8 | -------------------------------------------------------------------------------- /lib/tools/eventBus.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { createMessageBus } = require('@arpinum/messaging'); 4 | 5 | module.exports = createMessageBus; 6 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*.{js,json}] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | charset = utf-8 7 | indent_style = space 8 | indent_size = 2 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | module.exports = Object.assign( 2 | require('./domain'), 3 | require('./errors'), 4 | require('./query'), 5 | require('./test'), 6 | require('./tools'), 7 | require('./types'), 8 | require('./web') 9 | ); 10 | -------------------------------------------------------------------------------- /lib/test/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | FakeRequest: require('./fakeRequest'), 3 | FakeResponse: require('./fakeResponse'), 4 | MemoryEventStore: require('./memoryEventStore'), 5 | StubMessageBus: require('./stubMessageBus') 6 | }; 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # @arpinum/ddd [![Build Status](https://travis-ci.org/arpinum/js-ddd.svg?branch=master)](https://travis-ci.org/arpinum/js-ddd) 2 | 3 | *@arpinum/ddd* is a DDD framework CQRS and ES friendly. 4 | 5 | ## License 6 | 7 | [MIT](LICENSE) 8 | -------------------------------------------------------------------------------- /lib/types/repositoryContract.js: -------------------------------------------------------------------------------- 1 | const t = require('tcomb'); 2 | 3 | const RepositoryContract = t.interface( 4 | { 5 | getById: t.Function 6 | }, 7 | { name: 'RepositoryContract' } 8 | ); 9 | 10 | module.exports = RepositoryContract; 11 | -------------------------------------------------------------------------------- /lib/types/verbHandlerContract.js: -------------------------------------------------------------------------------- 1 | const t = require('tcomb'); 2 | 3 | const VerbHandlerContract = t.interface( 4 | { 5 | handle: t.Function 6 | }, 7 | { name: 'VerbHandlerContract' } 8 | ); 9 | 10 | module.exports = VerbHandlerContract; 11 | -------------------------------------------------------------------------------- /lib/web/objectToHtml.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | function objectToHtml(object) { 4 | return JSON.stringify(object, null, '\t') 5 | .replace(/\n/g, '
') 6 | .replace(/\t/g, '   '); 7 | } 8 | 9 | module.exports = objectToHtml; 10 | -------------------------------------------------------------------------------- /lib/tools/map.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | function indexArray(array, property) { 4 | return (array || []).reduce((results, object) => { 5 | results.set(object[property], object); 6 | return results; 7 | }, new Map()); 8 | } 9 | 10 | module.exports = { indexArray }; 11 | -------------------------------------------------------------------------------- /lib/errors/web/clientError.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const WebError = require('./webError'); 4 | 5 | class ClientError extends WebError { 6 | constructor(message, code) { 7 | super(message || 'Client error', code || 400); 8 | } 9 | } 10 | 11 | module.exports = ClientError; 12 | -------------------------------------------------------------------------------- /lib/errors/web/forbiddenError.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const ClientError = require('./clientError'); 4 | 5 | class ForbiddenError extends ClientError { 6 | constructor(message) { 7 | super(message || 'Forbidden', 403); 8 | } 9 | } 10 | 11 | module.exports = ForbiddenError; 12 | -------------------------------------------------------------------------------- /lib/errors/web/serverError.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const WebError = require('./webError'); 4 | 5 | class ServerError extends WebError { 6 | constructor(message, code) { 7 | super(message || 'Server error', code || 500); 8 | } 9 | } 10 | 11 | module.exports = ServerError; 12 | -------------------------------------------------------------------------------- /lib/types/aggregateRootContract.js: -------------------------------------------------------------------------------- 1 | const t = require('tcomb'); 2 | 3 | const AggregateRootContract = t.interface( 4 | { 5 | id: t.String, 6 | aggregateName: t.String 7 | }, 8 | { name: 'AggregateRootContract' } 9 | ); 10 | 11 | module.exports = AggregateRootContract; 12 | -------------------------------------------------------------------------------- /lib/errors/generic/technicalError.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const CustomError = require('./customError'); 4 | 5 | class TechnicalError extends CustomError { 6 | constructor(message) { 7 | super(message || 'Technical error'); 8 | } 9 | } 10 | 11 | module.exports = TechnicalError; 12 | -------------------------------------------------------------------------------- /lib/errors/web/resourceNotFoundError.js: -------------------------------------------------------------------------------- 1 | const ClientError = require('./clientError'); 2 | 3 | class ResourceNotFoundError extends ClientError { 4 | constructor(message) { 5 | super(message || 'Resource not found', 404); 6 | } 7 | } 8 | 9 | module.exports = ResourceNotFoundError; 10 | -------------------------------------------------------------------------------- /lib/errors/web/webError.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const CustomError = require('../generic/customError'); 4 | 5 | class WebError extends CustomError { 6 | constructor(message, code) { 7 | super(message); 8 | this.code = code; 9 | } 10 | } 11 | 12 | module.exports = WebError; 13 | -------------------------------------------------------------------------------- /lib/errors/generic/functionalError.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const CustomError = require('./customError'); 4 | 5 | class FunctionalError extends CustomError { 6 | constructor(message) { 7 | super(message || 'Functionnal error'); 8 | } 9 | } 10 | 11 | module.exports = FunctionalError; 12 | -------------------------------------------------------------------------------- /lib/types/messageHandlerContract.js: -------------------------------------------------------------------------------- 1 | const t = require('tcomb'); 2 | 3 | const MessageHandlerContract = t.interface( 4 | { 5 | handle: t.Function, 6 | messageName: t.String 7 | }, 8 | { name: 'MessageHandlerContract' } 9 | ); 10 | 11 | module.exports = MessageHandlerContract; 12 | -------------------------------------------------------------------------------- /lib/errors/web/unauthorizedError.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const ClientError = require('./clientError'); 4 | 5 | class UnauthorizedError extends ClientError { 6 | constructor(message) { 7 | super(message || 'Unauthorized', 401); 8 | } 9 | } 10 | 11 | module.exports = UnauthorizedError; 12 | -------------------------------------------------------------------------------- /lib/tools/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | createEventBus: require('./eventBus'), 3 | map: require('./map'), 4 | object: require('./object'), 5 | sanitize: require('./sanitize'), 6 | stream: require('./stream'), 7 | uuid: require('./uuid'), 8 | validate: require('./validate') 9 | }; 10 | -------------------------------------------------------------------------------- /lib/query/queryBus.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { createMessageBus } = require('@arpinum/messaging'); 4 | 5 | function createQueryBus(creation) { 6 | return createMessageBus( 7 | Object.assign({}, { exclusiveHandlers: true }, creation) 8 | ); 9 | } 10 | 11 | module.exports = createQueryBus; 12 | -------------------------------------------------------------------------------- /lib/errors/validationError.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const FunctionalError = require('./generic/functionalError'); 4 | 5 | class ValidationError extends FunctionalError { 6 | constructor(message) { 7 | super(message || 'Validation error'); 8 | } 9 | } 10 | 11 | module.exports = ValidationError; 12 | -------------------------------------------------------------------------------- /lib/types/projectionUpdaterContract.js: -------------------------------------------------------------------------------- 1 | const t = require('tcomb'); 2 | 3 | const ProjectionUpdaterContract = t.interface( 4 | { 5 | build: t.Function, 6 | registerToBus: t.Function 7 | }, 8 | { name: 'ProjectionUpdaterContract' } 9 | ); 10 | 11 | module.exports = ProjectionUpdaterContract; 12 | -------------------------------------------------------------------------------- /lib/domain/commandBus.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { createMessageBus } = require('@arpinum/messaging'); 4 | 5 | function createCommandBus(creation) { 6 | return createMessageBus( 7 | Object.assign({}, { exclusiveHandlers: true }, creation) 8 | ); 9 | } 10 | 11 | module.exports = createCommandBus; 12 | -------------------------------------------------------------------------------- /lib/errors/authorizationError.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const FunctionalError = require('./generic/functionalError'); 4 | 5 | class AuthorizationError extends FunctionalError { 6 | constructor(message) { 7 | super(message || 'Authorization error'); 8 | } 9 | } 10 | 11 | module.exports = AuthorizationError; 12 | -------------------------------------------------------------------------------- /lib/errors/entityNotFoundError.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const FunctionalError = require('./generic/functionalError'); 4 | 5 | class EntityNotFoundError extends FunctionalError { 6 | constructor(criteria) { 7 | super(`No entity for ${JSON.stringify(criteria)}`); 8 | } 9 | } 10 | 11 | module.exports = EntityNotFoundError; 12 | -------------------------------------------------------------------------------- /lib/web/headerParsing.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const _ = require('lodash'); 4 | 5 | function get(headers, name) { 6 | if (!name) { 7 | return null; 8 | } 9 | return _.find(headers, (value, key) => { 10 | return key.toLowerCase() === name.toLowerCase(); 11 | }); 12 | } 13 | 14 | module.exports = { 15 | get 16 | }; 17 | -------------------------------------------------------------------------------- /lib/errors/generic/customError.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | class CustomError extends Error { 4 | constructor(message = 'Error') { 5 | super(message); 6 | Error.captureStackTrace(this, this.constructor); 7 | this.message = this.message; 8 | this.name = this.constructor.name; 9 | } 10 | } 11 | 12 | module.exports = CustomError; 13 | -------------------------------------------------------------------------------- /lib/types/eventStoreContract.js: -------------------------------------------------------------------------------- 1 | const t = require('tcomb'); 2 | 3 | const EventStoreContract = t.interface( 4 | { 5 | add: t.Function, 6 | addAll: t.Function, 7 | eventsFromAggregate: t.Function, 8 | eventsFromTypes: t.Function 9 | }, 10 | { name: 'EventStoreContract' } 11 | ); 12 | 13 | module.exports = EventStoreContract; 14 | -------------------------------------------------------------------------------- /lib/domain/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | AggregateRoot: require('./aggregateRoot'), 3 | createCommandBus: require('./commandBus'), 4 | Repository: require('./repository'), 5 | addEventsToStoreDecorator: require('./commandDecorators/addEventsToStoreDecorator'), 6 | broadcastEventsDecorator: require('./commandDecorators/broadcastEventsDecorator') 7 | }; 8 | -------------------------------------------------------------------------------- /lib/errors/web/webError.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const WebError = require('./webError'); 4 | 5 | describe('A web error', () => { 6 | it('could be create with a message and code', () => { 7 | let error = new WebError('the message', 501); 8 | 9 | error.message.should.equal('the message'); 10 | error.code.should.equal(501); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /lib/errors/queriedObjectNotFoundError.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const FunctionalError = require('./generic/functionalError'); 4 | 5 | class QueriedObjectNotFoundError extends FunctionalError { 6 | constructor(criteria) { 7 | super(`Queried object not found for ${JSON.stringify(criteria)}`); 8 | } 9 | } 10 | 11 | module.exports = QueriedObjectNotFoundError; 12 | -------------------------------------------------------------------------------- /lib/test/stubMessageBus.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | class StubMessageBus { 4 | constructor() { 5 | const sinon = require('sinon'); 6 | this.broadcastAll = sinon.stub().returns(Promise.resolve()); 7 | this.broadcast = sinon.stub().returns(Promise.resolve()); 8 | this.register = sinon.stub(); 9 | } 10 | } 11 | 12 | module.exports = StubMessageBus; 13 | -------------------------------------------------------------------------------- /lib/test/fakeRequest.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | class FakeRequest { 4 | constructor() { 5 | this.query = {}; 6 | this.body = {}; 7 | this.params = {}; 8 | } 9 | 10 | param(name) { 11 | return this.params[name]; 12 | } 13 | 14 | withBody(body) { 15 | this.body = body; 16 | return this; 17 | } 18 | } 19 | 20 | module.exports = { FakeRequest }; 21 | -------------------------------------------------------------------------------- /lib/errors/entityNotFoundError.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const EntityNotFoundError = require('./entityNotFoundError'); 4 | 5 | describe('The entity not found error', () => { 6 | it('should be created with a message based on criteria', () => { 7 | let error = new EntityNotFoundError({ a: 'criterion' }); 8 | 9 | error.message.should.equal('No entity for {"a":"criterion"}'); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /lib/web/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | headerParsing: require('./headerParsing'), 3 | latencySimulatorMiddleware: require('./middlewares/latencySimulatorMiddleware'), 4 | limitFieldsMiddleware: require('./middlewares/limitFieldsMiddleware'), 5 | prettyMiddleware: require('./middlewares/prettyMiddleware'), 6 | unhandledErrorMiddleware: require('./middlewares/unhandledErrorMiddleware'), 7 | objectToHtml: require('./objectToHtml') 8 | }; 9 | -------------------------------------------------------------------------------- /lib/test/fakeResponse.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | class FakeResponse { 4 | constructor() { 5 | const sinon = require('sinon'); 6 | 7 | this.send = sinon.stub().returnsThis(); 8 | this.end = sinon.stub().returnsThis(); 9 | this.status = sinon.stub().returnsThis(); 10 | this.cookie = sinon.stub().returnsThis(); 11 | this.clearCookie = sinon.stub().returnsThis(); 12 | } 13 | } 14 | 15 | module.exports = FakeResponse; 16 | -------------------------------------------------------------------------------- /lib/domain/test/catEventHandlers.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const handlers = { 4 | CatCreated: (event, cat) => cat.updateWith({ age: event.payload.age }), 5 | CatNamed: (event, cat) => cat.updateWith({ name: event.payload.name }), 6 | FailingEvent: () => { 7 | throw new Error('bleh'); 8 | }, 9 | CatBirthdateDefined: (event, cat) => 10 | cat.updateWith({ birthDate: event.payload.birthDate }) 11 | }; 12 | 13 | module.exports = handlers; 14 | -------------------------------------------------------------------------------- /lib/errors/queriedObjectNotFoundError.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const QueriedObjectNotFoundError = require('./queriedObjectNotFoundError'); 4 | 5 | describe('The queried object not found error', () => { 6 | it('should be created with a message based on criteria', () => { 7 | let error = new QueriedObjectNotFoundError({ a: 'criterion' }); 8 | 9 | error.message.should.equal( 10 | 'Queried object not found for {"a":"criterion"}' 11 | ); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /lib/tools/uuid.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const uuid = require('node-uuid'); 4 | 5 | const regex = /.{8}-.{4}-.{4}-.{4}-.{12}/; 6 | 7 | let fixedUuid; 8 | 9 | function fix(uuid) { 10 | fixedUuid = uuid; 11 | } 12 | 13 | function reset() { 14 | fixedUuid = null; 15 | } 16 | 17 | function create() { 18 | if (fixedUuid) { 19 | return fixedUuid; 20 | } 21 | return uuid.v4(); 22 | } 23 | 24 | module.exports = { 25 | create, 26 | regex, 27 | fix, 28 | reset 29 | }; 30 | -------------------------------------------------------------------------------- /lib/errors/generic/customError.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const CustomError = require('./customError'); 4 | 5 | class MyError extends CustomError {} 6 | 7 | describe('A custom error', () => { 8 | it('can have a message', () => { 9 | let error = new CustomError('Error happened'); 10 | 11 | error.message.should.equal('Error happened'); 12 | }); 13 | 14 | it('should expose concrete error name', () => { 15 | let error = new MyError(); 16 | 17 | error.name = 'MyError'; 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /lib/errors/generic/technicalError.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const TechnicalError = require('./technicalError'); 4 | 5 | describe('A technical error', () => { 6 | it('should have a generic error message by default', () => { 7 | let error = new TechnicalError(); 8 | 9 | error.message.should.equal('Technical error'); 10 | }); 11 | 12 | it('should use provided message', () => { 13 | let error = new TechnicalError('Custom message'); 14 | 15 | error.message.should.equal('Custom message'); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /lib/errors/validationError.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const ValidationError = require('./validationError'); 4 | 5 | describe('The validation error', () => { 6 | it('should have a generic error message by default', () => { 7 | let error = new ValidationError(); 8 | 9 | error.message.should.equal('Validation error'); 10 | }); 11 | 12 | it('should allow default message overriding', () => { 13 | let error = new ValidationError('my error'); 14 | 15 | error.message.should.equal('my error'); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /lib/errors/generic/functionalError.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const FunctionalError = require('./functionalError'); 4 | 5 | describe('A functionnal error', () => { 6 | it('should have a generic error message by default', () => { 7 | let error = new FunctionalError(); 8 | 9 | error.message.should.equal('Functionnal error'); 10 | }); 11 | 12 | it('should use provided message', () => { 13 | let error = new FunctionalError('Custom message'); 14 | 15 | error.message.should.equal('Custom message'); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /lib/errors/authorizationError.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const AuthorizationError = require('./authorizationError'); 4 | 5 | describe('The authorization error', () => { 6 | it('should have a generic error message by default', () => { 7 | let error = new AuthorizationError(); 8 | 9 | error.message.should.equal('Authorization error'); 10 | }); 11 | 12 | it('should allow default message overriding', () => { 13 | let error = new AuthorizationError('my error'); 14 | 15 | error.message.should.equal('my error'); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /lib/domain/test/cat.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const t = require('tcomb'); 4 | const AggregateRoot = require('../aggregateRoot'); 5 | const catEventHandlers = require('./catEventHandlers'); 6 | 7 | const Creation = t.interface({ 8 | id: t.String, 9 | age: t.Integer, 10 | name: t.maybe(t.String), 11 | birthDate: t.maybe(t.Date) 12 | }); 13 | 14 | class Cat extends AggregateRoot { 15 | constructor(creation) { 16 | super(Creation(creation)); 17 | } 18 | 19 | get handlers() { 20 | return catEventHandlers; 21 | } 22 | } 23 | 24 | module.exports = Cat; 25 | -------------------------------------------------------------------------------- /lib/web/middlewares/latencySimulatorMiddleware.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const _ = require('lodash'); 4 | 5 | function latencySimulatorMiddleware(options) { 6 | let _options = _.defaults({}, options, { 7 | enabled: false, 8 | minDelayInMs: 200, 9 | maxDelayInMs: 600 10 | }); 11 | return (request, response, next) => { 12 | if (_options.enabled) { 13 | setTimeout(next, _.random(_options.minDelayInMs, _options.maxDelayInMs)); 14 | } else { 15 | next(); 16 | } 17 | }; 18 | } 19 | 20 | module.exports = latencySimulatorMiddleware; 21 | -------------------------------------------------------------------------------- /lib/web/middlewares/prettyMiddleware.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const _ = require('lodash'); 4 | const objectToHtml = require('../objectToHtml'); 5 | 6 | function prettyMiddleware() { 7 | return (request, response, next) => { 8 | if (request.query.pretty !== undefined) { 9 | let originalSend = response.send; 10 | response.send = object => { 11 | let body = object; 12 | if (_.isObject(object)) { 13 | body = objectToHtml(object); 14 | } 15 | originalSend.call(response, body); 16 | }; 17 | } 18 | next(); 19 | }; 20 | } 21 | 22 | module.exports = prettyMiddleware; 23 | -------------------------------------------------------------------------------- /lib/errors/web/forbiddenError.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const ForbiddenError = require('./forbiddenError'); 4 | 5 | describe('An forbidden error', () => { 6 | let error; 7 | 8 | beforeEach(() => { 9 | error = new ForbiddenError(); 10 | }); 11 | 12 | it('should have a default message', () => { 13 | error.message.should.equal('Forbidden'); 14 | }); 15 | 16 | it('could have a custom message', () => { 17 | let error = new ForbiddenError('Bleh'); 18 | 19 | error.message.should.equal('Bleh'); 20 | }); 21 | 22 | it('should have 403 as default code', () => { 23 | error.code.should.equal(403); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /lib/tools/validate.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const _ = require('lodash'); 4 | const t = require('tcomb-validation'); 5 | const { ValidationError } = require('../errors'); 6 | 7 | function validate(object) { 8 | return { 9 | isA 10 | }; 11 | 12 | function isA(type) { 13 | let validation = t.validate(object, type); 14 | if (!validation.isValid()) { 15 | let errors = _.map(validation.errors, 'message'); 16 | let error = new ValidationError( 17 | `Validation failed, a reason is: ${errors[0]}` 18 | ); 19 | error.errors = errors; 20 | throw error; 21 | } 22 | } 23 | } 24 | 25 | module.exports = validate; 26 | -------------------------------------------------------------------------------- /lib/errors/web/unauthorizedError.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const UnauthorizedError = require('./unauthorizedError'); 4 | 5 | describe('An unauthorized error', () => { 6 | let error; 7 | 8 | beforeEach(() => { 9 | error = new UnauthorizedError(); 10 | }); 11 | 12 | it('should have a default message', () => { 13 | error.message.should.equal('Unauthorized'); 14 | }); 15 | 16 | it('could have a custom message', () => { 17 | let error = new UnauthorizedError('Bleh'); 18 | 19 | error.message.should.equal('Bleh'); 20 | }); 21 | 22 | it('should have 401 as default code', () => { 23 | error.code.should.equal(401); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /lib/types/index.js: -------------------------------------------------------------------------------- 1 | const t = require('tcomb'); 2 | const { contracts: messagingContracts } = require('@arpinum/messaging'); 3 | const { contracts: logContracts } = require('@arpinum/log'); 4 | 5 | module.exports = Object.assign( 6 | { 7 | AggregateRootContract: require('./aggregateRootContract'), 8 | EventStoreContract: require('./eventStoreContract'), 9 | MessageHandlerContract: require('./messageHandlerContract'), 10 | ProjectionUpdaterContract: require('./projectionUpdaterContract'), 11 | RepositoryContract: require('./repositoryContract'), 12 | VerbHandlerContract: require('./verbHandlerContract') 13 | }, 14 | messagingContracts(t), 15 | logContracts(t) 16 | ); 17 | -------------------------------------------------------------------------------- /lib/errors/web/resourceNotFoundError.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const ResourceNotFoundError = require('./resourceNotFoundError'); 4 | 5 | describe('A resource not found error', () => { 6 | let error; 7 | 8 | beforeEach(() => { 9 | error = new ResourceNotFoundError(); 10 | }); 11 | 12 | it('should have a default message', () => { 13 | error.message.should.equal('Resource not found'); 14 | }); 15 | 16 | it('could have a custom message', () => { 17 | let error = new ResourceNotFoundError('Bleh'); 18 | 19 | error.message.should.equal('Bleh'); 20 | }); 21 | 22 | it('should have 404 as default code', () => { 23 | error.code.should.equal(404); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /lib/errors/web/serverError.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const ServerError = require('./serverError'); 4 | 5 | describe('A server error', () => { 6 | it('could be created with a message and code', () => { 7 | let error = new ServerError('my message', 501); 8 | 9 | error.message.should.equal('my message'); 10 | error.code.should.equal(501); 11 | }); 12 | 13 | it('should have a default message', () => { 14 | let error = new ServerError(); 15 | 16 | error.message.should.equal('Server error'); 17 | }); 18 | 19 | it('should have 500 as default message', () => { 20 | let error = new ServerError('my message'); 21 | 22 | error.code.should.equal(500); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /lib/errors/web/clientError.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const ClientError = require('./clientError'); 4 | 5 | describe('A client error', () => { 6 | it('could be created with a specific code and message', () => { 7 | let error = new ClientError('my message', 403); 8 | 9 | error.message.should.equal('my message'); 10 | error.code.should.equal(403); 11 | }); 12 | 13 | it('should have a generic message by default', () => { 14 | let error = new ClientError(); 15 | 16 | error.message.should.equal('Client error'); 17 | }); 18 | 19 | it('should have a 400 as default code', () => { 20 | let error = new ClientError('my message'); 21 | 22 | error.code.should.equal(400); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /lib/tools/object.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const _ = require('lodash'); 4 | 5 | function shrink(object) { 6 | return _.reduce( 7 | object, 8 | (result, value, key) => { 9 | if (value !== undefined) { 10 | if (isObjectLike(value)) { 11 | result[key] = shrink(value); 12 | } else { 13 | result[key] = value; 14 | } 15 | } 16 | return result; 17 | }, 18 | {} 19 | ); 20 | 21 | function isObjectLike(value) { 22 | return ( 23 | _.isObject(value) && 24 | !_.isDate(value) && 25 | !_.isArray(value) && 26 | !_.isRegExp(value) && 27 | !_.isFunction(value) 28 | ); 29 | } 30 | } 31 | 32 | module.exports = { shrink }; 33 | -------------------------------------------------------------------------------- /lib/errors/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | AuthorizationError: require('./authorizationError'), 3 | EntityNotFoundError: require('./entityNotFoundError'), 4 | QueriedObjectNotFoundError: require('./queriedObjectNotFoundError'), 5 | ValidationError: require('./validationError'), 6 | FunctionalError: require('./generic/functionalError'), 7 | TechnicalError: require('./generic/technicalError'), 8 | ClientError: require('./web/clientError'), 9 | ForbiddenError: require('./web/forbiddenError'), 10 | ResourceNotFoundError: require('./web/resourceNotFoundError'), 11 | ServerError: require('./web/serverError'), 12 | UnauthorizedError: require('./web/unauthorizedError'), 13 | WebError: require('./web/webError') 14 | }; 15 | -------------------------------------------------------------------------------- /lib/web/middlewares/limitFieldsMiddleware.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const _ = require('lodash'); 4 | 5 | function limitFieldsMiddleware() { 6 | return (request, response, next) => { 7 | if (request.query.fields !== undefined) { 8 | let originalSend = response.send; 9 | response.send = object => { 10 | let body = object; 11 | if (_.isArray(object)) { 12 | body = _.map(object, element => 13 | _.pick(element, request.query.fields) 14 | ); 15 | } else if (_.isObject(object)) { 16 | body = _.pick(object, request.query.fields); 17 | } 18 | originalSend.call(response, body); 19 | }; 20 | } 21 | next(); 22 | }; 23 | } 24 | 25 | module.exports = limitFieldsMiddleware; 26 | -------------------------------------------------------------------------------- /lib/domain/commandDecorators/broadcastEventsDecorator.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'); 2 | const t = require('tcomb'); 3 | const { createLogger } = require('@arpinum/log'); 4 | const { MessageBusContract, LoggerContract } = require('../../types'); 5 | 6 | const Creation = t.interface( 7 | { 8 | eventBus: MessageBusContract, 9 | options: t.maybe( 10 | t.interface({ 11 | log: t.maybe(LoggerContract) 12 | }) 13 | ) 14 | }, 15 | { strict: true } 16 | ); 17 | 18 | function broadcastEventsDecorator(creation) { 19 | let { eventBus, options: rawOptions } = Creation(creation); 20 | let options = _.defaults({}, rawOptions, { 21 | log: createLogger({ fileName: __filename }) 22 | }); 23 | 24 | return result => { 25 | options.log.debug('Broadcasting events'); 26 | return eventBus.postAll(result.events).then(() => result); 27 | }; 28 | } 29 | 30 | module.exports = broadcastEventsDecorator; 31 | -------------------------------------------------------------------------------- /lib/domain/commandDecorators/addEventsToStoreDecorator.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'); 2 | const t = require('tcomb'); 3 | const { createLogger } = require('@arpinum/log'); 4 | const { EventStoreContract, LoggerContract } = require('../../types'); 5 | 6 | const Creation = t.interface( 7 | { 8 | eventStore: EventStoreContract, 9 | options: t.maybe( 10 | t.interface({ 11 | log: t.maybe(LoggerContract) 12 | }) 13 | ) 14 | }, 15 | { strict: true } 16 | ); 17 | 18 | function addEventsToStoreDecorator(creation) { 19 | let { eventStore, options: rawOptions } = Creation(creation); 20 | let options = _.defaults({}, rawOptions, { 21 | log: createLogger({ fileName: __filename }) 22 | }); 23 | 24 | return result => { 25 | options.log.debug('Adding events to store'); 26 | return eventStore.addAll(result.events).then(() => result); 27 | }; 28 | } 29 | 30 | module.exports = addEventsToStoreDecorator; 31 | -------------------------------------------------------------------------------- /lib/web/headerParsing.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const should = require('chai').should(); 4 | const headerParsing = require('./headerParsing'); 5 | 6 | describe('Header parsing', () => { 7 | it('should get specific header', () => { 8 | headerParsing.get({ header: 'value' }, 'header').should.equal('value'); 9 | }); 10 | 11 | it('should get null if no specific header', () => { 12 | should.not.exist(headerParsing.get({ header: 'value' })); 13 | }); 14 | 15 | it('should get null if header cannot be found', () => { 16 | should.not.exist(headerParsing.get({ header: 'value' }, 'unknown')); 17 | }); 18 | 19 | it('should get null if there are no headers', () => { 20 | should.not.exist(headerParsing.get(null, 'unknown')); 21 | }); 22 | 23 | it('should get specific header without case sensitivity', () => { 24 | headerParsing 25 | .get({ 'x-header': 'value' }, 'X-Header') 26 | .should.equal('value'); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /lib/domain/test/catRepository.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const t = require('tcomb'); 4 | const Cat = require('./cat'); 5 | const { EventStoreContract } = require('../../types'); 6 | const Repository = require('../repository'); 7 | 8 | const Creation = t.interface( 9 | { 10 | eventStore: EventStoreContract 11 | }, 12 | { strict: true } 13 | ); 14 | 15 | class CatRepository extends Repository { 16 | constructor(creation) { 17 | super(parentCreation()); 18 | 19 | function parentCreation() { 20 | let { eventStore } = Creation(creation); 21 | return { 22 | eventStore, 23 | AggregateRootType: Cat, 24 | options: { 25 | beforeEventApplication: [stringToBirthDate] 26 | } 27 | }; 28 | } 29 | } 30 | } 31 | 32 | function stringToBirthDate(event) { 33 | if (event.payload.birthDate) { 34 | return Object.assign({}, event, { 35 | payload: { birthDate: new Date(event.payload.birthDate) } 36 | }); 37 | } 38 | return event; 39 | } 40 | 41 | module.exports = CatRepository; 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | (The MIT License) 2 | 3 | Copyright (C) 2017, Arpinum 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | 'Software'), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /lib/domain/commandDecorators/addEventsToStoreDecorator.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { MemoryEventStore } = require('../../test'); 4 | const addEventsToStoreDecorator = require('./addEventsToStoreDecorator'); 5 | 6 | describe('Add events to store decorator', () => { 7 | let eventStore; 8 | let decorator; 9 | 10 | beforeEach(() => { 11 | eventStore = new MemoryEventStore(); 12 | decorator = addEventsToStoreDecorator({ eventStore }); 13 | }); 14 | 15 | it('should save resulting events', () => { 16 | let events = [{ type: 'CatCreated' }, { type: 'CatNamed' }]; 17 | 18 | let decoration = decorator({ events }); 19 | 20 | return decoration.then(() => { 21 | eventStore.events.length.should.equal(2); 22 | eventStore.events[0].type.should.equal('CatCreated'); 23 | eventStore.events[1].type.should.equal('CatNamed'); 24 | }); 25 | }); 26 | 27 | it('should return result as it', () => { 28 | let events = [{ type: 'CatCreated' }]; 29 | 30 | let decoration = decorator({ events }); 31 | 32 | return decoration.then(result => { 33 | result.should.deep.equal({ events }); 34 | }); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /lib/domain/aggregateRoot.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const t = require('tcomb'); 4 | 5 | const Creation = t.interface({ 6 | id: t.String 7 | }); 8 | 9 | class AggregateRoot { 10 | constructor(creation) { 11 | Object.assign(this, Creation(creation)); 12 | } 13 | 14 | get aggregateName() { 15 | return this.constructor.name; 16 | } 17 | 18 | get handlers() { 19 | return {}; 20 | } 21 | 22 | updateWith(update) { 23 | const creation = Object.assign({}, this, update); 24 | return new this.constructor(creation); 25 | } 26 | 27 | createEvent(message) { 28 | const aggregatePart = { 29 | aggregate: { id: this.id, type: this.aggregateName } 30 | }; 31 | const datePart = { date: new Date() }; 32 | return Object.assign({}, message, datePart, aggregatePart); 33 | } 34 | 35 | applyEvent(event) { 36 | const handler = this.handlers[event.type] || (() => this); 37 | return handler(event, this); 38 | } 39 | 40 | static bootstrap(creation) { 41 | const { type, id } = creation; 42 | const root = Object.create(type.prototype, { 43 | id: { value: id, enumerable: true } 44 | }); 45 | return root; 46 | } 47 | } 48 | 49 | module.exports = AggregateRoot; 50 | -------------------------------------------------------------------------------- /lib/domain/commandDecorators/broadcastEventsDecorator.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { createEventBus } = require('../../tools'); 4 | const broadcastEventsDecorator = require('./broadcastEventsDecorator'); 5 | 6 | describe('Broadcast events decorator', () => { 7 | let eventBus; 8 | let decorator; 9 | 10 | beforeEach(() => { 11 | eventBus = createEventBus({ log: () => undefined }); 12 | decorator = broadcastEventsDecorator({ eventBus }); 13 | }); 14 | 15 | it('should broadcast resulting events', () => { 16 | let events = [{ type: 'CatCreated' }, { type: 'CatNamed' }]; 17 | let broadcasts = []; 18 | eventBus.register('CatCreated', () => { 19 | broadcasts.push('CatCreated'); 20 | }); 21 | eventBus.register('CatNamed', () => { 22 | broadcasts.push('CatNamed'); 23 | }); 24 | 25 | let decoration = decorator({ events }); 26 | 27 | return decoration.then(() => { 28 | broadcasts.should.deep.equal(['CatCreated', 'CatNamed']); 29 | }); 30 | }); 31 | 32 | it('should return result as it', () => { 33 | let events = [{ type: 'CatCreated' }]; 34 | 35 | let decoration = decorator({ events }); 36 | 37 | return decoration.then(result => { 38 | result.should.deep.equal({ events }); 39 | }); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@arpinum/ddd", 3 | "version": "4.0.0-beta14", 4 | "description": "DDD framework CQRS and ES friendly", 5 | "main": "./lib/index.js", 6 | "files": [ 7 | "lib" 8 | ], 9 | "scripts": { 10 | "eslint": "./node_modules/.bin/eslint .", 11 | "mocha": "LOG_LEVEL=off ./node_modules/.bin/mocha lib", 12 | "test": "npm run mocha; code=$?; npm run eslint; exit $((${code}+$?))", 13 | "tdd": "watch --wait 1 \"npm test\" lib", 14 | "prettier": "prettier --write \"**/*.js\"", 15 | "preversion": "npm test", 16 | "postversion": "git push && git push --tags && npm publish" 17 | }, 18 | "repository": "arpinum/js-ddd", 19 | "keywords": [ 20 | "DDD", 21 | "CQRS", 22 | "ES", 23 | "Arpinum" 24 | ], 25 | "author": "Arpinum", 26 | "license": "MIT", 27 | "dependencies": { 28 | "@arpinum/log": "4.0.0", 29 | "@arpinum/messaging": "2.1.0", 30 | "@arpinum/promising": "2.1.0", 31 | "lodash": "4.17.4", 32 | "node-uuid": "1.4.8", 33 | "tcomb": "3.2.24", 34 | "tcomb-validation": "3.4.1" 35 | }, 36 | "devDependencies": { 37 | "chai": "4.1.2", 38 | "eslint": "4.11.0", 39 | "eslint-config-arpinum": "5.0.0", 40 | "mocha": "4.0.1", 41 | "prettier": "1.8.2", 42 | "sinon": "4.1.2", 43 | "watch": "1.0.2" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /lib/test/memoryEventStore.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const _ = require('lodash'); 4 | const { wrap } = require('@arpinum/promising'); 5 | const { stream } = require('../tools'); 6 | 7 | class MemoryEventStore { 8 | constructor() { 9 | this.events = []; 10 | } 11 | 12 | add(event) { 13 | let self = this; 14 | return wrap(() => { 15 | this.events.push(Object.assign({}, event, { id: generateId() })); 16 | })(); 17 | 18 | function generateId() { 19 | return self.events.length; 20 | } 21 | } 22 | 23 | addAll(events) { 24 | return wrap(() => events.forEach(event => this.add(event)))(); 25 | } 26 | 27 | eventsFromAggregate(id, type) { 28 | let results = _.filter( 29 | this.events, 30 | e => 31 | _.get(e, 'aggregate.id') === id && _.get(e, 'aggregate.type') === type 32 | ); 33 | return stream.createArrayStream(results); 34 | } 35 | 36 | eventsFromTypes(types, newerThanThisEventId) { 37 | let results = this.allEventsFromTypes(types, newerThanThisEventId); 38 | return stream.createArrayStream(results); 39 | } 40 | 41 | allEventsFromTypes(types, newerThanThisEventId) { 42 | return _.filter(this.events, e => { 43 | let valid = _.includes(types, e.type); 44 | if (!_.isNil(newerThanThisEventId)) { 45 | valid = valid && e.id > newerThanThisEventId; 46 | } 47 | return valid; 48 | }); 49 | } 50 | } 51 | 52 | module.exports = MemoryEventStore; 53 | -------------------------------------------------------------------------------- /lib/tools/stream.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const _ = require('lodash'); 4 | const { mapSeries, createQueue } = require('@arpinum/promising'); 5 | const { Readable } = require('stream'); 6 | 7 | class ArrayStream extends Readable { 8 | constructor(elements) { 9 | super({ objectMode: true }); 10 | this._elements = elements; 11 | } 12 | 13 | _read() { 14 | for (let element of this._elements) { 15 | this.push(element); 16 | } 17 | this.push(null); 18 | } 19 | } 20 | 21 | function createArrayStream(elements) { 22 | return new ArrayStream(elements); 23 | } 24 | 25 | function executePromiseInBatch(stream, promiseFunction, options) { 26 | let _options = _.defaults({}, options, { batchSize: 1000 }); 27 | let flushQueue = createQueue(); 28 | let batch = []; 29 | return new Promise((resolve, reject) => { 30 | stream.on('data', data => { 31 | batch.push(data); 32 | if (batch.length === _options.batchSize) { 33 | stream.pause(); 34 | enqueueFlushBatch() 35 | .then(() => stream.resume()) 36 | .catch(reject); 37 | } 38 | }); 39 | stream.on('error', error => { 40 | reject(error); 41 | }); 42 | stream.once('end', () => { 43 | enqueueFlushBatch() 44 | .then(resolve) 45 | .catch(reject); 46 | }); 47 | }); 48 | 49 | function enqueueFlushBatch() { 50 | return flushQueue.enqueue(flushBatch); 51 | } 52 | 53 | function flushBatch() { 54 | return mapSeries(promiseFunction, batch).then(() => { 55 | batch = []; 56 | }); 57 | } 58 | } 59 | 60 | module.exports = { 61 | createArrayStream, 62 | executePromiseInBatch 63 | }; 64 | -------------------------------------------------------------------------------- /lib/web/middlewares/unhandledErrorMiddleware.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const _ = require('lodash'); 4 | const { createLogger } = require('@arpinum/log'); 5 | const { 6 | AuthorizationError, 7 | FunctionalError, 8 | QueriedObjectNotFoundError, 9 | EntityNotFoundError, 10 | ClientError, 11 | ServerError, 12 | WebError, 13 | ResourceNotFoundError, 14 | ForbiddenError 15 | } = require('../../errors'); 16 | 17 | function unhandledErrorMiddleware(options) { 18 | let _options = _.defaults({}, options, { 19 | log: createLogger({ fileName: __filename }), 20 | verboseWebErrors: false 21 | }); 22 | return (error, request, response, next) => { 23 | void next; 24 | logError(error); 25 | let webError = webErrorFrom(error); 26 | response.status(webError.code).send({ error: webError }); 27 | }; 28 | 29 | function logError(error) { 30 | let message = error.stack || error.message; 31 | _options.log.error(`Unhandled error (${message})`); 32 | } 33 | 34 | function webErrorFrom(error) { 35 | if ( 36 | isA(error, EntityNotFoundError) || 37 | isA(error, QueriedObjectNotFoundError) 38 | ) { 39 | return translateError(error, ResourceNotFoundError); 40 | } 41 | if (isA(error, AuthorizationError)) { 42 | return translateError(error, ForbiddenError); 43 | } 44 | if (isA(error, WebError)) { 45 | return error; 46 | } 47 | if (isA(error, FunctionalError)) { 48 | return translateError(error, ClientError); 49 | } 50 | return serverErrorFrom(error); 51 | } 52 | 53 | function serverErrorFrom(error) { 54 | let baseError = _options.verboseWebErrors ? error : {}; 55 | return translateError(baseError, ServerError); 56 | } 57 | 58 | function translateError(sourceError, DestinationType) { 59 | let result = new DestinationType(sourceError.message); 60 | return _.defaults(result, sourceError); 61 | } 62 | 63 | function isA(error, constructor) { 64 | return error instanceof constructor; 65 | } 66 | } 67 | 68 | module.exports = unhandledErrorMiddleware; 69 | -------------------------------------------------------------------------------- /lib/tools/object.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { shrink } = require('./object'); 4 | 5 | describe('object module', () => { 6 | context('when shrinking', () => { 7 | it('should remove undefined properties', () => { 8 | let object = { a: undefined }; 9 | 10 | shrink(object).should.deep.equal({}); 11 | }); 12 | 13 | it('should remove nested undefined properties', () => { 14 | let object = { a: { b: { c: undefined } } }; 15 | 16 | shrink(object).should.deep.equal({ a: { b: {} } }); 17 | }); 18 | 19 | it('should keep defined properties', () => { 20 | let object = { a: null, b: '', c: {}, d: Number.NaN }; 21 | 22 | shrink(object).should.deep.equal({ 23 | a: null, 24 | b: '', 25 | c: {}, 26 | d: Number.NaN 27 | }); 28 | }); 29 | 30 | it('should handle both defined and undefined properties', () => { 31 | let object = { a: undefined, b: 'hello' }; 32 | 33 | shrink(object).should.deep.equal({ b: 'hello' }); 34 | }); 35 | 36 | it('wont alter inputs', () => { 37 | let object = { a: undefined }; 38 | 39 | let result = shrink(object); 40 | 41 | result.should.not.equal(object); 42 | Object.keys(object).should.include('a'); 43 | }); 44 | 45 | it('should preserve date objects', () => { 46 | let date = new Date(); 47 | let object = { a: date }; 48 | 49 | shrink(object).should.deep.equal({ a: date }); 50 | }); 51 | 52 | it('should preserve arrays', () => { 53 | let array = ['home']; 54 | let object = { a: array }; 55 | 56 | shrink(object).should.deep.equal({ a: array }); 57 | }); 58 | 59 | it('should preserve regexp', () => { 60 | let regexp = /.*/; 61 | let object = { a: regexp }; 62 | 63 | shrink(object).should.deep.equal({ a: regexp }); 64 | }); 65 | 66 | it('should preserve function', () => { 67 | let func = () => 'ok'; 68 | let object = { a: func }; 69 | 70 | shrink(object) 71 | .a() 72 | .should.equal('ok'); 73 | }); 74 | }); 75 | }); 76 | -------------------------------------------------------------------------------- /lib/query/projectionUpdater.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const _ = require('lodash'); 4 | const t = require('tcomb'); 5 | const co = require('co'); 6 | const { wrap } = require('@arpinum/promising'); 7 | const { createLogger } = require('@arpinum/log'); 8 | const { EventStoreContract, LoggerContract } = require('../types'); 9 | const { executePromiseInBatch } = require('../tools').stream; 10 | 11 | const Creation = t.interface({ 12 | eventStore: EventStoreContract, 13 | isEmpty: t.Function, 14 | handlers: t.Object, 15 | options: t.maybe( 16 | t.interface({ 17 | log: t.maybe(LoggerContract) 18 | }) 19 | ) 20 | }); 21 | 22 | class ProjectionUpdater { 23 | constructor(creation) { 24 | let { eventStore, isEmpty, handlers, options } = Creation(creation); 25 | this._eventStore = eventStore; 26 | this._isEmpty = isEmpty; 27 | this._handlers = handlers; 28 | this._options = Object.assign( 29 | {}, 30 | { log: createLogger({ fileName: __filename }), options } 31 | ); 32 | } 33 | 34 | build() { 35 | let self = this; 36 | return co(function*() { 37 | if (yield self._isEmpty()) { 38 | return buildFromEvents(); 39 | } 40 | return Promise.resolve(); 41 | }); 42 | 43 | function buildFromEvents() { 44 | return co(function*() { 45 | self._options.log.debug('Projection build started'); 46 | let stream = self._eventStore.eventsFromTypes(eventTypes()); 47 | yield executePromiseInBatch(stream, event => self._handleEvent(event)); 48 | self._options.log.debug('Projection build done'); 49 | }); 50 | } 51 | 52 | function eventTypes() { 53 | return _.keys(self._handlers); 54 | } 55 | } 56 | 57 | registerToBus(eventBus) { 58 | _.forEach(this._handlers, (handler, type) => { 59 | eventBus.register(type, event => this._handleEvent(event)); 60 | }); 61 | } 62 | 63 | _handleEvent(event) { 64 | let self = this; 65 | return wrap(self._handlers[event.type])(event).catch(rejection => { 66 | self._options.log.error(`Update failed on event ${event.id}`, rejection); 67 | throw rejection; 68 | }); 69 | } 70 | } 71 | 72 | module.exports = ProjectionUpdater; 73 | -------------------------------------------------------------------------------- /lib/tools/sanitize.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const _ = require('lodash'); 4 | const { FunctionalError } = require('../errors'); 5 | 6 | class Converter { 7 | constructor(object, propertyName) { 8 | this._object = object; 9 | this._propertyName = propertyName; 10 | } 11 | 12 | toDate() { 13 | let convert = rawProperty => { 14 | let parsing = Date.parse(rawProperty); 15 | if (isNaN(parsing)) { 16 | return { 17 | success: false, 18 | invalidType: 'date' 19 | }; 20 | } 21 | return { 22 | success: true, 23 | value: new Date(parsing) 24 | }; 25 | }; 26 | return this._convert(convert); 27 | } 28 | 29 | toInteger() { 30 | let convert = rawProperty => { 31 | let parsing = Number.parseInt(rawProperty); 32 | if (Number.isNaN(parsing)) { 33 | return { 34 | success: false, 35 | invalidType: 'integer' 36 | }; 37 | } 38 | return { 39 | success: true, 40 | value: parsing 41 | }; 42 | }; 43 | return this._convert(convert); 44 | } 45 | 46 | toBoolean() { 47 | let convert = rawProperty => { 48 | let parsing = { true: true, false: false }[rawProperty]; 49 | if (parsing === undefined) { 50 | return { 51 | success: false, 52 | invalidType: 'boolean' 53 | }; 54 | } 55 | return { 56 | success: true, 57 | value: parsing 58 | }; 59 | }; 60 | return this._convert(convert); 61 | } 62 | 63 | _convert(convert) { 64 | let clone = _.clone(this._object); 65 | let rawProperty = _.get(clone, this._propertyName); 66 | if (rawProperty === undefined || rawProperty === null) { 67 | return clone; 68 | } 69 | let conversion = convert(rawProperty); 70 | if (!conversion.success) { 71 | const message = 72 | `The property ${this._propertyName} ` + 73 | `is not a valid ${conversion.invalidType}`; 74 | throw new FunctionalError(message); 75 | } 76 | _.set(clone, this._propertyName, conversion.value); 77 | return clone; 78 | } 79 | } 80 | 81 | class Sanitize { 82 | constructor(object) { 83 | this._object = object; 84 | } 85 | 86 | convert(propertyName) { 87 | return new Converter(this._object, propertyName); 88 | } 89 | } 90 | 91 | function sanitize(object) { 92 | return new Sanitize(object); 93 | } 94 | 95 | module.exports = { sanitize }; 96 | -------------------------------------------------------------------------------- /lib/tools/stream.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { wrap } = require('@arpinum/promising'); 4 | const stream = require('./stream'); 5 | 6 | describe('The stream module', () => { 7 | it('should execute promise in batch', () => { 8 | let integerStream = stream.createArrayStream([1, 2, 3, 4]); 9 | let readIntegers = []; 10 | let promiseFunc = wrap(integer => readIntegers.push(integer)); 11 | 12 | let executePromiseInBatch = stream.executePromiseInBatch( 13 | integerStream, 14 | promiseFunc, 15 | { batchSize: 1 } 16 | ); 17 | 18 | return executePromiseInBatch.then(() => { 19 | readIntegers.should.deep.equal([1, 2, 3, 4]); 20 | }); 21 | }); 22 | 23 | it('should consider last batch though incomplete', () => { 24 | let integerStream = stream.createArrayStream([1, 2, 3, 4]); 25 | let readIntegers = []; 26 | let promiseFunc = wrap(integer => readIntegers.push(integer)); 27 | 28 | let executePromiseInBatch = stream.executePromiseInBatch( 29 | integerStream, 30 | promiseFunc, 31 | { batchSize: 3 } 32 | ); 33 | 34 | return executePromiseInBatch.then(() => { 35 | readIntegers.should.deep.equal([1, 2, 3, 4]); 36 | }); 37 | }); 38 | 39 | it('should reject if any error during batch processing', () => { 40 | let integerStream = stream.createArrayStream([1, 2, 3, 4]); 41 | let promiseFunc = wrap(integer => { 42 | if (integer === 1) { 43 | throw new Error('bleh'); 44 | } 45 | }); 46 | 47 | let executePromiseInBatch = stream.executePromiseInBatch( 48 | integerStream, 49 | promiseFunc, 50 | { batchSize: 200 } 51 | ); 52 | 53 | return executePromiseInBatch.then( 54 | () => Promise.reject(new Error('Should fail')), 55 | rejection => rejection.message.should.equal('bleh') 56 | ); 57 | }); 58 | 59 | it('should reject if any error at the end', () => { 60 | let integerStream = stream.createArrayStream([1, 2, 3, 4]); 61 | let promiseFunc = wrap(integer => { 62 | if (integer === 4) { 63 | throw new Error('bleh'); 64 | } 65 | }); 66 | 67 | let executePromiseInBatch = stream.executePromiseInBatch( 68 | integerStream, 69 | promiseFunc, 70 | { batchSize: 200 } 71 | ); 72 | 73 | return executePromiseInBatch.then( 74 | () => Promise.reject(new Error('Should fail')), 75 | rejection => rejection.message.should.equal('bleh') 76 | ); 77 | }); 78 | }); 79 | -------------------------------------------------------------------------------- /lib/domain/repository.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const _ = require('lodash'); 4 | const t = require('tcomb'); 5 | const { EntityNotFoundError, TechnicalError } = require('../errors'); 6 | const { EventStoreContract, LoggerContract } = require('../types'); 7 | const { createLogger } = require('@arpinum/log'); 8 | const AggregateRoot = require('./aggregateRoot'); 9 | 10 | const Creation = t.interface( 11 | { 12 | eventStore: EventStoreContract, 13 | AggregateRootType: t.Function, 14 | options: t.maybe( 15 | t.interface({ 16 | log: t.maybe(LoggerContract), 17 | beforeEventApplication: t.maybe(t.list(t.Function)) 18 | }) 19 | ) 20 | }, 21 | { strict: true } 22 | ); 23 | 24 | class Repository { 25 | constructor(creation) { 26 | let { eventStore, AggregateRootType, options } = Creation(creation); 27 | this._eventStore = eventStore; 28 | this._AggregateRootType = AggregateRootType; 29 | this._options = _.defaults({}, options, { 30 | log: createLogger({ fileName: __filename }), 31 | beforeEventApplication: [] 32 | }); 33 | } 34 | 35 | getById(id, options) { 36 | let self = this; 37 | let getByIdOptions = _.defaults({}, options, { maybeMissing: false }); 38 | let result = AggregateRoot.bootstrap({ type: this._AggregateRootType, id }); 39 | let stream = this._eventStore.eventsFromAggregate(id, result.aggregateName); 40 | let eventCount = 0; 41 | return new Promise((resolve, reject) => { 42 | stream.on('data', event => { 43 | eventCount++; 44 | result = applyEvent(event, result, reject); 45 | }); 46 | stream.on('error', error => { 47 | reject(error); 48 | }); 49 | stream.once('end', () => { 50 | if (eventCount === 0) { 51 | handleMissing(resolve, reject); 52 | } else { 53 | resolve(result); 54 | } 55 | }); 56 | }); 57 | 58 | function applyEvent(event, aggregateRoot, reject) { 59 | try { 60 | self._options.log.debug( 61 | `Applying ${event.type} on ${event.aggregate.id}` 62 | ); 63 | return _.flow( 64 | ...self._options.beforeEventApplication, 65 | aggregateRoot.applyEvent.bind(aggregateRoot) 66 | )(event); 67 | } catch (error) { 68 | self._options.log.error(`Error applying event ${event.type}`, error); 69 | reject(new TechnicalError('Event cannot be applied')); 70 | return null; 71 | } 72 | } 73 | 74 | function handleMissing(resolve, reject) { 75 | if (!getByIdOptions.maybeMissing) { 76 | reject(new EntityNotFoundError({ id })); 77 | } else { 78 | resolve(); 79 | } 80 | } 81 | } 82 | } 83 | 84 | module.exports = Repository; 85 | -------------------------------------------------------------------------------- /lib/domain/aggregateRoot.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { AggregateRootContract } = require('../types'); 4 | const Cat = require('./test/cat'); 5 | const AggregateRoot = require('./aggregateRoot'); 6 | 7 | describe('The aggregate root', () => { 8 | let aggregateRoot; 9 | 10 | beforeEach(() => { 11 | aggregateRoot = new Cat({ id: '42', age: 1 }); 12 | }); 13 | 14 | context('after creation', () => { 15 | it('should match AggregateRootContract', () => { 16 | AggregateRootContract.is(aggregateRoot).should.be.true; 17 | }); 18 | 19 | it('should have aggregateName matching type', () => { 20 | aggregateRoot.aggregateName.should.equal('Cat'); 21 | }); 22 | }); 23 | 24 | context('while bootstraping', () => { 25 | it('should create a typed and identified root', () => { 26 | let root = AggregateRoot.bootstrap({ type: Cat, id: '42' }); 27 | 28 | root.should.be.instanceOf(Cat); 29 | root.id.should.equal('42'); 30 | }); 31 | }); 32 | 33 | context('while creating event', () => { 34 | it('should use type and payload', () => { 35 | let event = aggregateRoot.createEvent({ 36 | type: 'Message', 37 | payload: { name: 'the event' } 38 | }); 39 | 40 | event.payload.should.deep.equal({ name: 'the event' }); 41 | }); 42 | 43 | it('should concern the corresponding aggregate', () => { 44 | let event = aggregateRoot.createEvent({ 45 | type: 'Message', 46 | payload: { name: 'the event' } 47 | }); 48 | 49 | event.aggregate.should.deep.equal({ 50 | id: '42', 51 | type: 'Cat' 52 | }); 53 | }); 54 | }); 55 | 56 | context('while applying events', () => { 57 | it('should return vanilla object if event is unknown', () => { 58 | let result = aggregateRoot.applyEvent({ type: 'Unknown' }); 59 | 60 | result.should.equal(aggregateRoot); 61 | }); 62 | }); 63 | 64 | context('while updating', () => { 65 | let updated; 66 | 67 | beforeEach(() => { 68 | updated = aggregateRoot.updateWith({ name: 'woofy' }); 69 | }); 70 | 71 | it('should return a different instance', () => { 72 | updated.should.not.equal(aggregateRoot); 73 | }); 74 | 75 | it('should return an updated instance', () => { 76 | updated.id.should.equal('42'); 77 | updated.name.should.equal('woofy'); 78 | }); 79 | 80 | it('should keep the original type', () => { 81 | updated.constructor.name.should.equal('Cat'); 82 | }); 83 | it('should keep original properties', () => { 84 | let aggregateRoot = new Cat({ id: '42', age: 10 }); 85 | 86 | updated = aggregateRoot.updateWith({ name: 'woofy' }); 87 | 88 | updated.age.should.equal(10); 89 | }); 90 | }); 91 | }); 92 | -------------------------------------------------------------------------------- /lib/test/memoryEventStore.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { EventStoreContract } = require('../types'); 4 | const MemoryEventStore = require('./memoryEventStore'); 5 | 6 | describe('The memory event store', () => { 7 | let eventStore; 8 | 9 | beforeEach(() => { 10 | eventStore = new MemoryEventStore(); 11 | }); 12 | 13 | context('after creation', () => { 14 | it('should match EventStoreContract', () => { 15 | EventStoreContract.is(eventStore).should.be.true; 16 | }); 17 | }); 18 | 19 | it('should add an event and set its id', () => { 20 | let add = eventStore.add({ the: 'event' }); 21 | 22 | return add.then(() => { 23 | eventStore.events.should.deep.equal([{ id: 0, the: 'event' }]); 24 | }); 25 | }); 26 | 27 | it('should add multiple events', () => { 28 | let addAll = eventStore.addAll([ 29 | { first: 'first event' }, 30 | { second: 'second event' } 31 | ]); 32 | 33 | return addAll.then(() => { 34 | eventStore.events.should.deep.equal([ 35 | { id: 0, first: 'first event' }, 36 | { id: 1, second: 'second event' } 37 | ]); 38 | }); 39 | }); 40 | 41 | it('should find events from a aggregate', () => { 42 | eventStore.events.push( 43 | { id: 'event1', aggregate: { id: 'wrongAggregate', type: 'wrongType' } }, 44 | { id: 'event2', aggregate: { id: 'rightAggregate', type: 'rightType' } }, 45 | { id: 'event3', aggregate: { id: 'rightAggregate', type: 'wrongType' } }, 46 | { id: 'event4', aggregate: { id: 'rightAggregate', type: 'rightType' } }, 47 | { id: 'event5', aggregate: { id: 'wrongAggregate', type: 'wrongType' } } 48 | ); 49 | 50 | let eventsFromAggregate = eventStore.eventsFromAggregate( 51 | 'rightAggregate', 52 | 'rightType' 53 | ); 54 | 55 | return arrayFromStream(eventsFromAggregate).then(results => { 56 | results.should.deep.equal([ 57 | { 58 | id: 'event2', 59 | aggregate: { id: 'rightAggregate', type: 'rightType' } 60 | }, 61 | { id: 'event4', aggregate: { id: 'rightAggregate', type: 'rightType' } } 62 | ]); 63 | }); 64 | }); 65 | 66 | it('should find events from types', () => { 67 | eventStore.events.push( 68 | { id: 'event1', type: 'type1' }, 69 | { id: 'event2', type: 'type2' }, 70 | { id: 'event3', type: 'type2' }, 71 | { id: 'event4', type: 'type3' } 72 | ); 73 | 74 | let eventsFromTypes = eventStore.eventsFromTypes(['type2', 'type3']); 75 | 76 | return arrayFromStream(eventsFromTypes).then(results => { 77 | results.should.deep.equal([ 78 | { id: 'event2', type: 'type2' }, 79 | { id: 'event3', type: 'type2' }, 80 | { id: 'event4', type: 'type3' } 81 | ]); 82 | }); 83 | }); 84 | 85 | function arrayFromStream(stream) { 86 | return new Promise(resolve => { 87 | let elements = []; 88 | stream.on('data', element => elements.push(element)); 89 | stream.once('end', () => resolve(elements)); 90 | }); 91 | } 92 | }); 93 | -------------------------------------------------------------------------------- /lib/tools/sanitize.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { sanitize } = require('./sanitize'); 4 | const { FunctionalError } = require('../errors'); 5 | 6 | describe('The sanitize module', () => { 7 | context('about integer', () => { 8 | it('should convert a valid integer property', () => { 9 | let object = { int: '12' }; 10 | 11 | let result = sanitize(object) 12 | .convert('int') 13 | .toInteger(); 14 | 15 | result.should.deep.equal({ int: 12 }); 16 | }); 17 | 18 | it('should fail if the property is defined but cannot be converted', () => { 19 | let object = { int: 'not an int' }; 20 | 21 | let conversion = () => 22 | sanitize(object) 23 | .convert('int') 24 | .toInteger(); 25 | 26 | conversion.should.throw( 27 | FunctionalError, 28 | 'The property int is not a valid integer' 29 | ); 30 | }); 31 | }); 32 | 33 | context('about boolean', () => { 34 | it('should convert a valid truthy boolean property', () => { 35 | let object = { bool: 'true' }; 36 | 37 | let result = sanitize(object) 38 | .convert('bool') 39 | .toBoolean(); 40 | 41 | result.bool.should.be.true; 42 | }); 43 | 44 | it('should convert a valid falsy boolean property', () => { 45 | let object = { bool: 'false' }; 46 | 47 | let result = sanitize(object) 48 | .convert('bool') 49 | .toBoolean(); 50 | 51 | result.bool.should.be.false; 52 | }); 53 | 54 | it('should fail if the property is defined but cannot be converted', () => { 55 | let object = { bool: 'not a bool' }; 56 | 57 | let conversion = () => 58 | sanitize(object) 59 | .convert('bool') 60 | .toBoolean(); 61 | 62 | conversion.should.throw( 63 | FunctionalError, 64 | 'The property bool is not a valid boolean' 65 | ); 66 | }); 67 | }); 68 | 69 | context('about date', () => { 70 | it('should convert a valid date property', () => { 71 | let date = new Date('1996', '12', '03'); 72 | let object = { date: date.toISOString() }; 73 | 74 | let result = sanitize(object) 75 | .convert('date') 76 | .toDate(); 77 | 78 | result.should.deep.equal({ date }); 79 | }); 80 | 81 | it('should convert a deeply defined property', () => { 82 | let date = new Date('1996', '12', '03'); 83 | let object = { the: { super: { date: date.toISOString() } } }; 84 | 85 | let result = sanitize(object) 86 | .convert('the.super.date') 87 | .toDate(); 88 | 89 | result.should.deep.equal({ the: { super: { date } } }); 90 | }); 91 | 92 | it('should do nothing if the property is undefined', () => { 93 | let object = { nothing: 'todo' }; 94 | 95 | let result = sanitize(object) 96 | .convert('date') 97 | .toDate(); 98 | 99 | result.should.deep.equal({ nothing: 'todo' }); 100 | }); 101 | 102 | it('should do nothing if the property is null', () => { 103 | let object = { nothing: 'todo', date: null }; 104 | 105 | let result = sanitize(object) 106 | .convert('date') 107 | .toDate(); 108 | 109 | result.should.deep.equal({ nothing: 'todo', date: null }); 110 | }); 111 | 112 | it('should fail if the property is defined but cannot be converted', () => { 113 | let object = { theDate: 'not a date' }; 114 | 115 | let conversion = () => 116 | sanitize(object) 117 | .convert('theDate') 118 | .toDate(); 119 | 120 | conversion.should.throw( 121 | FunctionalError, 122 | 'The property theDate is not a valid date' 123 | ); 124 | }); 125 | }); 126 | }); 127 | -------------------------------------------------------------------------------- /lib/query/projectionUpdater.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { MemoryEventStore } = require('../test'); 4 | const { createEventBus } = require('../tools'); 5 | const ProjectionUpdater = require('./projectionUpdater'); 6 | 7 | describe('The projection updater', () => { 8 | let handlings; 9 | 10 | class MyUpdater extends ProjectionUpdater { 11 | constructor(creation) { 12 | super( 13 | Object.assign( 14 | { 15 | handlers: handlers(), 16 | isEmpty: () => Promise.resolve(true) 17 | }, 18 | creation 19 | ) 20 | ); 21 | 22 | function handlers() { 23 | return { 24 | SomethingHappened: event => { 25 | handlings.push({ type: 'SomethingHappened', event }); 26 | return Promise.resolve(); 27 | }, 28 | StuffOccurred: event => { 29 | handlings.push({ type: 'StuffOccurred', event }); 30 | return Promise.resolve(); 31 | } 32 | }; 33 | } 34 | } 35 | } 36 | 37 | let eventStore; 38 | let eventBus; 39 | let updater; 40 | 41 | beforeEach(() => { 42 | eventStore = new MemoryEventStore(); 43 | eventBus = createEventBus({ log: () => undefined }); 44 | handlings = []; 45 | updater = new MyUpdater({ eventStore }); 46 | }); 47 | 48 | context('during build', () => { 49 | it('should apply relevant events if is empty', () => { 50 | eventStore.events.push( 51 | { id: '1', type: 'SomethingHappened' }, 52 | { id: '2', type: 'IDoNotCare' }, 53 | { id: '3', type: 'StuffOccurred' } 54 | ); 55 | 56 | let ready = updater.build(); 57 | 58 | return ready.then(() => { 59 | handlings.should.deep.equal([ 60 | { 61 | type: 'SomethingHappened', 62 | event: { id: '1', type: 'SomethingHappened' } 63 | }, 64 | { type: 'StuffOccurred', event: { id: '3', type: 'StuffOccurred' } } 65 | ]); 66 | }); 67 | }); 68 | 69 | it('wont apply events if is not empty', () => { 70 | let updater = new MyUpdater({ 71 | eventStore, 72 | isEmpty: () => Promise.resolve(false) 73 | }); 74 | eventStore.events.push({ id: '1', type: 'SomethingHappened' }); 75 | 76 | let ready = updater.build(); 77 | 78 | return ready.then(() => { 79 | handlings.should.be.empty; 80 | }); 81 | }); 82 | }); 83 | 84 | context('while listening to event bus', () => { 85 | it('should handle relevant events', () => { 86 | let event = { type: 'SomethingHappened', payload: { the: 'payload' } }; 87 | updater.registerToBus(eventBus); 88 | 89 | let broadcast = eventBus.post(event); 90 | 91 | return broadcast.then(() => { 92 | handlings.should.deep.equal([{ type: 'SomethingHappened', event }]); 93 | }); 94 | }); 95 | 96 | it('may handle event synchronically', () => { 97 | let updater = new MyUpdater({ 98 | eventStore, 99 | handlers: { 100 | SomethingHappened: () => { 101 | handlings.push('SomethingHappened'); 102 | } 103 | } 104 | }); 105 | updater.registerToBus(eventBus); 106 | 107 | let broadcast = eventBus.post({ 108 | type: 'SomethingHappened', 109 | payload: { the: 'payload' } 110 | }); 111 | 112 | return broadcast.then(() => { 113 | handlings.should.deep.equal(['SomethingHappened']); 114 | }); 115 | }); 116 | 117 | it("won't handle irrelevant events", () => { 118 | updater.registerToBus(eventBus); 119 | 120 | let broadcast = eventBus.post({ 121 | type: 'IDoNotCare', 122 | payload: { the: 'payload' } 123 | }); 124 | 125 | return broadcast.then(() => { 126 | handlings.should.be.empty; 127 | }); 128 | }); 129 | }); 130 | }); 131 | -------------------------------------------------------------------------------- /lib/web/middlewares/unhandledErrorMiddleware.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const sinon = require('sinon'); 4 | const { FakeResponse } = require('../../test'); 5 | const { 6 | AuthorizationError, 7 | FunctionalError, 8 | TechnicalError, 9 | QueriedObjectNotFoundError, 10 | EntityNotFoundError, 11 | ClientError, 12 | ServerError, 13 | ForbiddenError, 14 | ResourceNotFoundError 15 | } = require('../../errors'); 16 | const unhandledErrorMiddleware = require('./unhandledErrorMiddleware'); 17 | 18 | describe('The unhandled error middleware', () => { 19 | let middleware; 20 | let response; 21 | 22 | beforeEach(() => { 23 | middleware = unhandledErrorMiddleware(); 24 | response = new FakeResponse(); 25 | }); 26 | 27 | it('should send a server error by default', () => { 28 | let error = new Error('the error'); 29 | let middleware = unhandledErrorMiddleware({ verboseWebErrors: true }); 30 | 31 | middleware(error, null, response); 32 | 33 | sinon.assert.calledWith(response.status, 500); 34 | errorSentIs(new ServerError('the error')); 35 | }); 36 | 37 | it('should hide the detailed message based on configuration', () => { 38 | let error = new TechnicalError('very technical message'); 39 | error.uselessDetails = 'very useless for user'; 40 | let middleware = unhandledErrorMiddleware({ verboseWebErrors: false }); 41 | 42 | middleware(error, null, response); 43 | 44 | errorSentIs(new ServerError()); 45 | }); 46 | 47 | it('should send a client error for functionnal errors', () => { 48 | let error = new FunctionalError('badaboom'); 49 | 50 | middleware(error, null, response); 51 | 52 | sinon.assert.calledWith(response.status, 400); 53 | errorSentIs(new ClientError('badaboom', 400)); 54 | }); 55 | 56 | it('should preserve data stored in errors', () => { 57 | let error = Object.assign(new FunctionalError('badaboom'), { 58 | data: { the: 'data' }, 59 | info: 3 60 | }); 61 | 62 | middleware(error, null, response); 63 | 64 | sinon.assert.calledWith(response.status, 400); 65 | let expected = Object.assign(new ClientError('badaboom'), { 66 | data: { the: 'data' }, 67 | info: 3 68 | }); 69 | errorSentIs(expected); 70 | }); 71 | 72 | it('should send a 404 for an entity not found error', () => { 73 | let error = new EntityNotFoundError({ id: '33' }); 74 | 75 | middleware(error, null, response); 76 | 77 | sinon.assert.calledWith(response.status, 404); 78 | let message = `No entity for ${JSON.stringify({ id: '33' })}`; 79 | errorSentIs(new ResourceNotFoundError(message)); 80 | }); 81 | 82 | it('should send a 404 for a queried object not found error', () => { 83 | let error = new QueriedObjectNotFoundError({ id: '33' }); 84 | 85 | middleware(error, null, response); 86 | 87 | sinon.assert.calledWith(response.status, 404); 88 | let message = `Queried object not found for ${JSON.stringify({ 89 | id: '33' 90 | })}`; 91 | errorSentIs(new ResourceNotFoundError(message)); 92 | }); 93 | 94 | it('should send a 403 for an authorization error', () => { 95 | let error = new AuthorizationError('You cannot!'); 96 | 97 | middleware(error, null, response); 98 | 99 | sinon.assert.calledWith(response.status, 403); 100 | errorSentIs(new ForbiddenError('You cannot!')); 101 | }); 102 | 103 | it('should send the provided error code if present', () => { 104 | let error = new ClientError('not found', 404); 105 | 106 | middleware(error, null, response); 107 | 108 | sinon.assert.calledWith(response.status, 404); 109 | errorSentIs(new ClientError('not found', 404)); 110 | }); 111 | 112 | function errorSentIs(expected) { 113 | let error = response.send.lastCall.args[0].error; 114 | error.should.be.instanceOf(expected.constructor); 115 | Object.assign({}, error).should.deep.equal(Object.assign({}, expected)); 116 | } 117 | }); 118 | -------------------------------------------------------------------------------- /lib/domain/repository.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const should = require('chai').should(); 4 | const { EntityNotFoundError, TechnicalError } = require('../errors'); 5 | const { MemoryEventStore } = require('../test'); 6 | const { RepositoryContract } = require('../types'); 7 | const Cat = require('./test/cat'); 8 | const CatRepository = require('./test/catRepository'); 9 | const Repository = require('./repository'); 10 | 11 | describe('The repository', () => { 12 | let eventStore; 13 | let repository; 14 | 15 | beforeEach(() => { 16 | eventStore = new MemoryEventStore(); 17 | repository = new CatRepository({ eventStore }); 18 | }); 19 | 20 | context('after creation', () => { 21 | it('should match RepositoryContract', () => { 22 | let repository = new Repository({ 23 | eventStore, 24 | AggregateRootType: Cat 25 | }); 26 | 27 | RepositoryContract.is(repository).should.be.true; 28 | }); 29 | }); 30 | 31 | context('while getting by id', () => { 32 | it("should apply all aggregate root's events", () => { 33 | eventStore.events.push( 34 | { 35 | id: '1', 36 | type: 'CatCreated', 37 | aggregate: { type: 'Cat', id: '42' }, 38 | payload: { age: 1 } 39 | }, 40 | { 41 | id: '2', 42 | type: 'StuffOccurred', 43 | aggregate: { type: 'Dog', id: 'dog_id' }, 44 | payload: { name: 'Wulfy' } 45 | }, 46 | { 47 | id: '3', 48 | type: 'CatNamed', 49 | aggregate: { type: 'Cat', id: '42' }, 50 | payload: { name: 'Garfield' } 51 | }, 52 | { 53 | id: '4', 54 | type: 'CatNamed', 55 | aggregate: { type: 'Cat', id: 'another_id' }, 56 | payload: { name: 'Isidor' } 57 | } 58 | ); 59 | 60 | let getById = repository.getById('42'); 61 | 62 | return getById.then(cat => { 63 | cat.should.include({ id: '42', name: 'Garfield', age: 1 }); 64 | }); 65 | }); 66 | 67 | it('should fail if event cannot be applied', () => { 68 | eventStore.events.push({ 69 | id: '1', 70 | type: 'FailingEvent', 71 | aggregate: { type: 'Cat', id: '42' } 72 | }); 73 | 74 | let getById = repository.getById('42'); 75 | 76 | return getById.then( 77 | () => Promise.reject(new Error('Should fail')), 78 | rejection => { 79 | rejection.should.be.instanceOf(TechnicalError); 80 | rejection.message.should.equal('Event cannot be applied'); 81 | } 82 | ); 83 | }); 84 | 85 | it('should fail if root is missing', () => { 86 | let getById = repository.getById('missing_id'); 87 | 88 | return getById.then( 89 | () => Promise.reject(new Error('Should fail')), 90 | rejection => { 91 | rejection.should.be.instanceOf(EntityNotFoundError); 92 | rejection.message.should.equal('No entity for {"id":"missing_id"}'); 93 | } 94 | ); 95 | }); 96 | 97 | it("won't fail if root may be missing", () => { 98 | let getById = repository.getById('missing_id', { maybeMissing: true }); 99 | 100 | return getById.then(result => { 101 | should.not.exist(result); 102 | }); 103 | }); 104 | 105 | it('should use decorators if provided before applying an event', () => { 106 | eventStore.events.push({ 107 | id: '1', 108 | type: 'CatCreated', 109 | aggregate: { type: 'Cat', id: '42' }, 110 | payload: { age: 1 } 111 | }); 112 | eventStore.events.push({ 113 | id: '2', 114 | type: 'CatBirthdateDefined', 115 | aggregate: { type: 'Cat', id: '42' }, 116 | payload: { birthDate: '2010-01-01' } 117 | }); 118 | 119 | let getById = repository.getById('42'); 120 | 121 | return getById.then(cat => { 122 | cat.birthDate.should.deep.equal(new Date('2010-01-01')); 123 | }); 124 | }); 125 | }); 126 | }); 127 | --------------------------------------------------------------------------------