From a3a31d51f0f72444dea23b5d3de2f64569fa9b7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Ko=C5=82ecki?= Date: Tue, 15 May 2018 23:55:24 +0200 Subject: [PATCH 1/9] First working implementation with tests --- .gitignore | 3 + command-bus.ts | 85 ----------------- package.json | 39 ++++++++ src/commandBus.ts | 10 ++ src/commander.ts | 35 +++++++ src/index.ts | 3 + src/middlewares/commandExecution.ts | 17 ++++ src/middlewares/commandLogger.ts | 44 +++++++++ src/middlewares/index.ts | 11 +++ src/resolvers/index.ts | 8 ++ src/resolvers/memory.ts | 32 +++++++ test/component/commander.test.ts | 51 ++++++++++ test/fakes/fakeLogger.ts | 26 +++++ test/fakes/fakeResolver.ts | 22 +++++ test/setup.ts | 6 ++ test/unit/commander.test.ts | 94 +++++++++++++++++++ .../unit/middlewares/commandExecution.test.ts | 85 +++++++++++++++++ test/unit/middlewares/commandLogger.test.ts | 90 ++++++++++++++++++ test/unit/resolvers/memory.test.ts | 62 ++++++++++++ tsconfig.json | 23 +++++ 20 files changed, 661 insertions(+), 85 deletions(-) create mode 100644 .gitignore delete mode 100644 command-bus.ts create mode 100644 package.json create mode 100644 src/commandBus.ts create mode 100644 src/commander.ts create mode 100644 src/index.ts create mode 100644 src/middlewares/commandExecution.ts create mode 100644 src/middlewares/commandLogger.ts create mode 100644 src/middlewares/index.ts create mode 100644 src/resolvers/index.ts create mode 100644 src/resolvers/memory.ts create mode 100644 test/component/commander.test.ts create mode 100644 test/fakes/fakeLogger.ts create mode 100644 test/fakes/fakeResolver.ts create mode 100644 test/setup.ts create mode 100644 test/unit/commander.test.ts create mode 100644 test/unit/middlewares/commandExecution.test.ts create mode 100644 test/unit/middlewares/commandLogger.test.ts create mode 100644 test/unit/resolvers/memory.test.ts create mode 100644 tsconfig.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1a99321 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +dist/ +node_modules/ +package-lock.json diff --git a/command-bus.ts b/command-bus.ts deleted file mode 100644 index 0b159a8..0000000 --- a/command-bus.ts +++ /dev/null @@ -1,85 +0,0 @@ -interface Command { - -} - - -interface CommandHandler { - handle(commmand: Command): any; -} - - -interface NextMiddleware { - executeNext(command: Command): any; -} - - -interface Middleware { - execute(command: Command, next: NextMiddleware): any; -} - - -interface CommandNameExtractor { - extract(command: Command): string; -} - - -interface HandlerLocator { - getHandlerForCommand(commandName: string): CommandHandler; -} - - -interface CommandBus { - handle(comand: Command): void; -} - - -class CommandHandlerMiddleware implements Middleware -{ - constructor(readonly handlerLocator: HandlerLocator, readonly commandNameExtractor: CommandNameExtractor) { - } - - execute(command: Command, next: NextMiddleware): any - { - let className: string = this.commandNameExtractor.extract(command); - - return this.handlerLocator.getHandlerForCommand(className).handle(command); - } -} - - -class InMemoryCommandBus implements CommandBus { - readonly commands: Command[]; - - handle(command: Command): void { - this.commands.push(command); - } -} - - -class Commander implements CommandBus { - private middlewareChain; - - constructor(readonly middlewares: Middleware[]) { - this.middlewareChain = this.createExcecutionChain(middlewares); - } - - handle(command: Command): void { - this.middlewareChain.executeNext(command); - } - - private createExcecutionChain(middlewares: Middleware[]): NextMiddleware { - let lastCallable: NextMiddleware = (new class implements NextMiddleware { - executeNext(command: Command): any { - } - }); - - for(let i: number = middlewares.length; i >= 0; i--) { - lastCallable = (new class implements NextMiddleware { - executeNext(command: Command): any { - return middlewares[i].execute(command, lastCallable); - } - }); - } - return lastCallable; - } -} diff --git a/package.json b/package.json new file mode 100644 index 0000000..448101d --- /dev/null +++ b/package.json @@ -0,0 +1,39 @@ +{ + "name": "commander", + "version": "0.1.0", + "description": "Command Bus For TypeScript", + "license": "MIT", + "main": "dist/src/index.js", + "directories": { + "test": "test" + }, + "scripts": { + "test": "npm run build && npm run run-test", + "build": "rm -rf dist && tsc", + "run-test": "mocha --file dist/test/setup.js 'dist/test/**/*.test.js'", + "watch-test": "mocha --require ts-node/register --watch --watch-extensions ts --file test/setup.ts 'test/**/*.test.ts'" + }, + "repository": { + "type": "git", + "url": "git+ssh://git@github.com/snapshotpl/commander.git" + }, + "keywords": [ + "command", + "bus", + "typescript", + "commander" + ], + "devDependencies": { + "@types/chai": "^4.1.3", + "@types/chai-as-promised": "^7.1.0", + "@types/mocha": "^5.2.0", + "@types/node": "^10.0.10", + "@types/sinon": "^4.3.3", + "chai": "^4.1.2", + "chai-as-promised": "^7.1.1", + "mocha": "^5.1.1", + "sinon": "^5.0.7", + "ts-node": "^6.0.3", + "typescript": "^2.8.3" + } +} diff --git a/src/commandBus.ts b/src/commandBus.ts new file mode 100644 index 0000000..f886afb --- /dev/null +++ b/src/commandBus.ts @@ -0,0 +1,10 @@ + +export interface Command { } // marking interface + +export interface Handler { + handle(command: C): Promise; +} + +export interface CommandBus { + handle(command: Command): Promise +} diff --git a/src/commander.ts b/src/commander.ts new file mode 100644 index 0000000..a6d6eb3 --- /dev/null +++ b/src/commander.ts @@ -0,0 +1,35 @@ +import { Middleware } from './middlewares'; +import { CommandBus, Command } from './commandBus'; + +type ExecutionChain = (command: Command) => Promise; + +export class Commander implements CommandBus { + + private executionChain: ExecutionChain; + + constructor(middlewares: Middleware[]) { + this.executionChain = this.createExecutionChain(middlewares); + } + + public async handle(command: Command): Promise { + return await this.executionChain(command); + } + + private createExecutionChain(middlewares: Middleware[]): ExecutionChain { + const last = () => { + // last callback in chain is no-op + return Promise.resolve(); + }; + + const reducer = ( + next: ExecutionChain, + middleware: Middleware + ): ExecutionChain => { + return (command: Command): Promise => { + return middleware.run(command, () => next(command)); + } + }; + + return middlewares.reverse().reduce(reducer, last); + } +} diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..813fc82 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,3 @@ + +export * from './commander'; +export * from './commandBus'; diff --git a/src/middlewares/commandExecution.ts b/src/middlewares/commandExecution.ts new file mode 100644 index 0000000..926734a --- /dev/null +++ b/src/middlewares/commandExecution.ts @@ -0,0 +1,17 @@ + +import { Middleware, NextMiddleware } from './index'; +import { Command, Handler } from '../commandBus'; +import { HandlerResolver } from '../resolvers' + +export class CommandExecutionMiddleware implements Middleware { + constructor( + private handlerResolver: HandlerResolver + ) { } + + public async run(command: Command, next: NextMiddleware): Promise { + const handler: Handler = this.handlerResolver.resolve(command); + const result = await handler.handle(command); + await next(); + return result; + } +} diff --git a/src/middlewares/commandLogger.ts b/src/middlewares/commandLogger.ts new file mode 100644 index 0000000..d7fd59c --- /dev/null +++ b/src/middlewares/commandLogger.ts @@ -0,0 +1,44 @@ + +import { Middleware, NextMiddleware } from './index'; +import { Command } from '../commandBus'; + + +export interface CommandLogger { + log(level: string, msg: string): CommandLogger; + log(level: string, msg: string, meta: any): CommandLogger; + log(level: string, msg: string, ...meta: any[]): CommandLogger; +} + +export interface CommandLoggerOptions { + level?: string; + errorLevel?: string; +} + +export class CommandLoggerMiddleware implements Middleware { + private logger: CommandLogger; + private level: string; + private errorLevel: string; + + constructor(logger: CommandLogger, options: CommandLoggerOptions = {}) { + this.logger = logger; + this.level = options.level || 'info'; + this.errorLevel = options.errorLevel || 'error'; + } + + public async run(command: Command, next: NextMiddleware): Promise { + const start = Date.now(); + const commandName = command.constructor.name; + + this.logger.log(this.level, 'Command %s dispatched', commandName); + + try { + return await next(); + } catch (err) { + this.logger.log(this.errorLevel, 'Command %s error. %s', commandName, err); + throw err; + } finally { + const time = Date.now() - start; + this.logger.log(this.level, 'Command %s time %dms', commandName, time); + } + } +} diff --git a/src/middlewares/index.ts b/src/middlewares/index.ts new file mode 100644 index 0000000..2e09040 --- /dev/null +++ b/src/middlewares/index.ts @@ -0,0 +1,11 @@ + +import { Command } from '../commandBus'; + +export type NextMiddleware = () => Promise; + +export interface Middleware { + run(command: Command, next: NextMiddleware): Promise; +} + +export * from './commandLogger'; +export * from './commandExecution'; diff --git a/src/resolvers/index.ts b/src/resolvers/index.ts new file mode 100644 index 0000000..6a31184 --- /dev/null +++ b/src/resolvers/index.ts @@ -0,0 +1,8 @@ + +import { Handler, Command } from '../commandBus'; + +export interface HandlerResolver { + resolve(command: C): Handler +} + +export * from './memory'; diff --git a/src/resolvers/memory.ts b/src/resolvers/memory.ts new file mode 100644 index 0000000..44a523b --- /dev/null +++ b/src/resolvers/memory.ts @@ -0,0 +1,32 @@ + +import { Handler, Command } from '../commandBus'; +import { HandlerResolver } from './index'; + +export type ConstructorOf = new (...args: any[]) => T; + +export class HandlerResolverInMemory implements HandlerResolver { + private handlers = new Map, Handler>(); + + public register(commandConstructor: ConstructorOf, handler: Handler): HandlerResolverInMemory { + this.handlers.set(commandConstructor, handler); + return this; + } + + public resolve(command: Command): Handler { + const handler = this.handlers.get(command.constructor as ConstructorOf); + + if (!handler) { + throw new Error(`Could not resolve handler for ${command.constructor.name}`); + } + + return handler; + } + + public count(): number { + return this.handlers.size; + } + + public entries(): IterableIterator<[ConstructorOf, Handler]> { + return this.handlers.entries(); + } +} diff --git a/test/component/commander.test.ts b/test/component/commander.test.ts new file mode 100644 index 0000000..18ed10b --- /dev/null +++ b/test/component/commander.test.ts @@ -0,0 +1,51 @@ + +import { expect } from "chai"; + +import { Commander } from '../../src/commander'; +import { Command, Handler } from '../../src/commandBus'; +import { + CommandLoggerMiddleware, + CommandExecutionMiddleware +} from '../../src/middlewares'; + +import { HandlerResolverInMemory } from '../../src/resolvers' +import { FakeLogger } from '../fakes/fakeLogger'; + + +describe("Commander combo", () => { + + class HelloCommand implements Command { + public firstName: string = ''; + public lastName: string = ''; + } + + class HelloHandler implements Handler { + public async handle(hello: HelloCommand): Promise { + return `Hello, ${hello.firstName} ${hello.lastName}`; + } + } + + let logger: FakeLogger; + + beforeEach(() => { + logger = new FakeLogger(); + }); + + it("should work", async () => { + const resolver = new HandlerResolverInMemory(); + resolver.register(HelloCommand, new HelloHandler()); + + const commander = new Commander([ + new CommandLoggerMiddleware(logger), + new CommandExecutionMiddleware(resolver), + ]); + + const hello = new HelloCommand(); + hello.firstName = 'John'; + hello.lastName = 'Doe'; + + const promise = commander.handle(hello); + + return expect(promise).to.eventually.be.equal('Hello, John Doe'); + }); +}); diff --git a/test/fakes/fakeLogger.ts b/test/fakes/fakeLogger.ts new file mode 100644 index 0000000..da44e4e --- /dev/null +++ b/test/fakes/fakeLogger.ts @@ -0,0 +1,26 @@ + +import { format } from 'util'; +import { CommandLogger } from '../../src/middlewares'; + + +export class FakeLogger implements CommandLogger { + + public messages: { level: string, msg: string, meta: any }[] = []; + + public log(level: string, msg: string, ...meta: any[]): CommandLogger { + const placeholders: string[] = msg.match(/%[sdifjoO]/g) || []; + const formatArgs: any[] = [msg].concat( + meta.slice(0, Math.min(placeholders.length, meta.length)) + ); + + msg = format.apply(null, formatArgs); + + this.messages.push({ + level: level, + msg: msg, + meta: placeholders.length === meta.length ? {} : meta[meta.length - 1] + }); + + return this; + } +} diff --git a/test/fakes/fakeResolver.ts b/test/fakes/fakeResolver.ts new file mode 100644 index 0000000..b2d3aa0 --- /dev/null +++ b/test/fakes/fakeResolver.ts @@ -0,0 +1,22 @@ + +import { HandlerResolver } from '../../src/resolvers'; +import { Command, Handler } from '../../src/commandBus'; + +/** + * Always resolve with handler which will return result from given callback. + */ +export class FakeResolver implements HandlerResolver { + constructor( + private handlerCallback: (c: Command) => Promise + ) { } + + resolve(command: C): Handler { + const callback = this.handlerCallback; + + return new class implements Handler { + handle(c: C): Promise { + return callback(c); + } + }; + } +} diff --git a/test/setup.ts b/test/setup.ts new file mode 100644 index 0000000..e53b819 --- /dev/null +++ b/test/setup.ts @@ -0,0 +1,6 @@ + +import 'mocha'; +import * as chai from 'chai'; +import * as chaiAsPromised from 'chai-as-promised'; + +chai.use(chaiAsPromised); diff --git a/test/unit/commander.test.ts b/test/unit/commander.test.ts new file mode 100644 index 0000000..c838623 --- /dev/null +++ b/test/unit/commander.test.ts @@ -0,0 +1,94 @@ + +import { expect } from "chai"; + +import { Commander } from '../../src/commander'; +import { Command } from '../../src/commandBus'; +import { Middleware, NextMiddleware } from '../../src/middlewares'; + + +class CallbackMiddleware implements Middleware { + constructor( + private callback: (command: Command, next: NextMiddleware) => Promise + ) { } + public run(command: Command, next: NextMiddleware): Promise { + return this.callback(command, next); + } +} + +describe("Commander", () => { + + const testCommand = new class implements Command { }; + + + it("should return promise", async () => { + const commander = new Commander([]); + const result = commander.handle(testCommand); + + expect(result).to.be.instanceOf(Promise); + }); + + it("should resolve with void when empty middlewares array is given", async () => { + const commander = new Commander([]); + const result = await commander.handle(testCommand); + + expect(result).to.be.undefined; + }); + + it("should resolve with value from Middleware", async () => { + const commander = new Commander([ + new CallbackMiddleware(async () => 'foo bar') + ]); + + const result = await commander.handle(testCommand); + + expect(result).to.be.equal('foo bar'); + }); + + it("should launch all middlewares in order of middleware next() call", async () => { + + const createPusher = (array: any[], item: any) => { + return async (command: Command, next: NextMiddleware) => { + await Promise.resolve(true); + await next(); + array.push(item); + } + } + + let counters: number[] = []; + + const commander = new Commander([ + new CallbackMiddleware(createPusher(counters, 0)), + new CallbackMiddleware(createPusher(counters, 1)), + new CallbackMiddleware(createPusher(counters, 2)), + ]); + + await commander.handle(testCommand); + + expect(counters).to.deep.equal([2, 1, 0]); + }); + + it("should resolve value from last middleware if all middlewares return next()", async () => { + + const commander = new Commander([ + new CallbackMiddleware((command: Command, next: NextMiddleware) => { + return next(); + }), + new CallbackMiddleware((command: Command, next: NextMiddleware) => { + return next(); + }), + new CallbackMiddleware((command: Command, next: NextMiddleware) => { + return next(); + }), + + new CallbackMiddleware(async (command: Command, next: NextMiddleware) => { + await next(); + return 'foo bar'; + }), + ]); + + const result = await commander.handle(testCommand); + + expect(result).to.be.equal('foo bar'); + }); + +}); diff --git a/test/unit/middlewares/commandExecution.test.ts b/test/unit/middlewares/commandExecution.test.ts new file mode 100644 index 0000000..4b06213 --- /dev/null +++ b/test/unit/middlewares/commandExecution.test.ts @@ -0,0 +1,85 @@ + +import { expect } from 'chai'; +import { stub } from 'sinon'; + +import { CommandExecutionMiddleware, NextMiddleware } from '../../../src/middlewares'; +import { Command } from '../../../src/commandBus'; + +import { FakeResolver } from '../../fakes/fakeResolver'; + +describe("Command Execution Middleware", () => { + + const noop: NextMiddleware = () => Promise.resolve('no op'); + const testCommand = new class implements Command { }; + + + it("should return value from resolved handler", async () => { + + const executor = new CommandExecutionMiddleware( + new FakeResolver(() => Promise.resolve('expected result')) + ); + + const result = await executor.run(testCommand, noop); + + expect(result).to.equal('expected result'); + }); + + it("should reject when handler rejects with the same reason", async () => { + + const givenReason = new Error(); + + const executor = new CommandExecutionMiddleware( + new FakeResolver(() => Promise.reject(givenReason)) + ); + + const promise = executor.run(testCommand, noop); + + return expect(promise).to.eventually.be.rejectedWith(givenReason); + }); + + it("should resolve efter next() resolves", async () => { + + const executor = new CommandExecutionMiddleware( + new FakeResolver(() => Promise.resolve()) + ); + + const next = stub(); + next.resolves(); + + const last = stub(); + + await executor.run(testCommand, next).then(last); + + expect(last.calledImmediatelyAfter(next)).to.be.true; + }); + + it("should call next() exacly once", async () => { + + const executor = new CommandExecutionMiddleware( + new FakeResolver(() => Promise.resolve()) + ); + + const next = stub(); + next.resolves(); + + await executor.run(testCommand, next); + + expect(next.calledOnce).to.be.true; + }); + + it("should reject promise when next() rejects with same reason", async () => { + + const executor = new CommandExecutionMiddleware( + new FakeResolver(() => Promise.resolve()) + ); + + const givenReason = new Error(); + + const next = stub(); + next.rejects(givenReason); + + const promise = executor.run(testCommand, next); + + return expect(promise).to.eventually.be.rejectedWith(givenReason); + }); +}); diff --git a/test/unit/middlewares/commandLogger.test.ts b/test/unit/middlewares/commandLogger.test.ts new file mode 100644 index 0000000..413bf28 --- /dev/null +++ b/test/unit/middlewares/commandLogger.test.ts @@ -0,0 +1,90 @@ + +import { expect } from 'chai'; +import { useFakeTimers, SinonFakeTimers } from 'sinon'; + +import { FakeLogger } from './../../fakes/fakeLogger'; +import { Command } from '../../../src/commandBus'; + +import { CommandLoggerMiddleware, NextMiddleware } from '../../../src/middlewares'; + +class TestCommand implements Command { +} + +describe("Command Logger Middleware", () => { + let logger: FakeLogger; + let commandLogger: CommandLoggerMiddleware; + let clock: SinonFakeTimers; + + beforeEach(() => { + logger = new FakeLogger(); + commandLogger = new CommandLoggerMiddleware(logger); + clock = useFakeTimers(); + }); + + afterEach(() => { + clock.restore(); + }); + + it("should log command dispatch and command execution time", async () => { + const next: NextMiddleware = () => { + clock.tick(25); + return Promise.resolve('foo bar'); + } + + await commandLogger.run(new TestCommand(), next); + + const infos = logger.messages.filter((entry) => entry.level === 'info'); + const errors = logger.messages.filter((entry) => entry.level === 'errors'); + + expect(infos.length).to.be.equal(2); + expect(errors.length).to.be.equal(0); + + expect(infos[0].msg).to.equal("Command TestCommand dispatched"); + expect(infos[1].msg).to.equal("Command TestCommand time 25ms"); + }); + + it("should resolve with same value as next()", async () => { + const next: NextMiddleware = () => { + return Promise.resolve('foo bar'); + } + + const promise = commandLogger.run(new TestCommand(), next); + + return expect(promise).to.eventually.be.equal('foo bar'); + }); + + it("should reject with same error as next() rejects", async () => { + const givenReason = new Error('foo bar'); + + const next: NextMiddleware = () => { + return Promise.reject(givenReason); + } + + const promise = commandLogger.run(new TestCommand(), next); + + return expect(promise).to.eventually.be.rejectedWith(givenReason); + }); + + it("should log command dispatch, command execution time and error when occured", async () => { + const next: NextMiddleware = () => { + clock.tick(125); + return Promise.reject(new Error('foo bar')); + } + + try { + await commandLogger.run(new TestCommand(), next); + } catch (e) { } + + const infos = logger.messages.filter((entry) => entry.level === 'info'); + const errors = logger.messages.filter((entry) => entry.level === 'error'); + + expect(infos.length).to.be.equal(2); + expect(errors.length).to.be.equal(1); + + expect(infos[0].msg).to.equal("Command TestCommand dispatched"); + expect(infos[1].msg).to.equal("Command TestCommand time 125ms"); + + expect(errors[0].msg).to.equal("Command TestCommand error. Error: foo bar"); + }); + +}); diff --git a/test/unit/resolvers/memory.test.ts b/test/unit/resolvers/memory.test.ts new file mode 100644 index 0000000..3d24051 --- /dev/null +++ b/test/unit/resolvers/memory.test.ts @@ -0,0 +1,62 @@ + +import { expect } from "chai"; + +import { Command, Handler } from '../../../src/commandBus'; +import { HandlerResolverInMemory } from '../../../src/resolvers'; + +describe("In Memory Handle Resolver", () => { + let resolver: HandlerResolverInMemory; + + class FooCommand implements Command { } + class BarCommand implements Command { } + class TestCommandWithNoHandler implements Command { } + + class FooHandler implements Handler { + handle(c: FooCommand): Promise { + return Promise.resolve(); + } + } + + class BarHandler implements Handler { + handle(c: FooCommand): Promise { + return Promise.resolve(); + } + } + + beforeEach(() => { + resolver = new HandlerResolverInMemory(); + }) + + it("should throw Error when no handler was registered", async () => { + expect(() => resolver.resolve(new FooCommand())) + .to.throw(Error); + }); + + it("should throw Error when handler was not registered for given command", async () => { + resolver.register(FooCommand, new FooHandler()); + + expect(() => resolver.resolve(new TestCommandWithNoHandler())) + .to.throw(Error); + }); + + it("return resolve with registered handler", async () => { + const givenHandler = new FooHandler(); + resolver.register(FooCommand, givenHandler); + resolver.register(BarCommand, new BarHandler()); + + const returnedHandler = resolver.resolve(new FooCommand()); + + expect(returnedHandler).to.be.equal(givenHandler); + }); + + it("resolve with last registered handler for given command constructor", async () => { + resolver.register(BarCommand, new BarHandler()); + resolver.register(FooCommand, new FooHandler()); + + const lastHandler = new FooHandler(); + resolver.register(FooCommand, lastHandler); + + expect(resolver.resolve(new FooCommand())) + .to.be.equal(lastHandler); + }); +}); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..917755f --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "outDir": "./dist/", + "target": "es5", + "module": "commonjs", + "moduleResolution": "node", + "experimentalDecorators": true, + "emitDecoratorMetadata": true, + "lib": [ + "es6", + "es5" + ], + "noImplicitAny": true, + "declaration": true, + "strict": true, + "pretty": true, + "noUnusedLocals": true + }, + "include": [ + "src/**/*", + "test/**/*" + ] +} From 44ca126a03163c0469e66b5a27f2d99422ba5a70 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Ko=C5=82ecki?= Date: Tue, 15 May 2018 23:55:56 +0200 Subject: [PATCH 2/9] Add .travis.yml --- .travis.yml | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..b543635 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,5 @@ +language: node_js +node_js: + - "8" + +script: npm test From fd2459f2a1466d928c987036a765c0320361979c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Ko=C5=82ecki?= Date: Wed, 16 May 2018 00:05:56 +0200 Subject: [PATCH 3/9] Customize .travis.yml --- .travis.yml | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index b543635..e1ef184 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,4 +2,12 @@ language: node_js node_js: - "8" -script: npm test +cache: + directories: + - node_modules + +install: + - npm install + +script: + - npm test From 0412086eb916b7821b14a2dd34c344ac0e6be8e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Ko=C5=82ecki?= Date: Thu, 17 May 2018 21:34:27 +0200 Subject: [PATCH 4/9] Add more component tests --- test/component/commander.test.ts | 59 +++++++++++++++++++++++++------- 1 file changed, 47 insertions(+), 12 deletions(-) diff --git a/test/component/commander.test.ts b/test/component/commander.test.ts index 18ed10b..03b12a8 100644 --- a/test/component/commander.test.ts +++ b/test/component/commander.test.ts @@ -12,40 +12,75 @@ import { HandlerResolverInMemory } from '../../src/resolvers' import { FakeLogger } from '../fakes/fakeLogger'; -describe("Commander combo", () => { +describe("Commander with middlewares combo", () => { class HelloCommand implements Command { - public firstName: string = ''; - public lastName: string = ''; + constructor( + public firstName: string, + public lastName: string + ) { } } class HelloHandler implements Handler { + public wait(timeout: number): Promise { + return new Promise(resolve => setTimeout(resolve, timeout)); + } + public async handle(hello: HelloCommand): Promise { + await this.wait(10); // simulate I/O waiting + + if (hello.lastName.length === 0) { + throw new Error('lastName is empty string'); + } + return `Hello, ${hello.firstName} ${hello.lastName}`; } } let logger: FakeLogger; + let commander: Commander; + let resolver: HandlerResolverInMemory; beforeEach(() => { logger = new FakeLogger(); - }); + resolver = new HandlerResolverInMemory(); - it("should work", async () => { - const resolver = new HandlerResolverInMemory(); resolver.register(HelloCommand, new HelloHandler()); - const commander = new Commander([ + commander = new Commander([ new CommandLoggerMiddleware(logger), new CommandExecutionMiddleware(resolver), ]); + }); - const hello = new HelloCommand(); - hello.firstName = 'John'; - hello.lastName = 'Doe'; - - const promise = commander.handle(hello); + it("should resolve with value from handler", async () => { + const promise = commander.handle(new HelloCommand('John', 'Doe')); return expect(promise).to.eventually.be.equal('Hello, John Doe'); }); + + it("should write info logs", async () => { + await commander.handle(new HelloCommand('John', 'Doe')); + + const infos = logger.messages.filter(entry => entry.level === 'info'); + + expect(infos.length).to.be.greaterThan(0); + }); + + it("should reject when handler rejects", async () => { + const promise = commander.handle(new HelloCommand('John', '')); + + return expect(promise).to.eventually.be.rejectedWith(Error, 'lastName is empty string'); + }); + + it("should write error logs when handler rejects", async () => { + try { + await commander.handle(new HelloCommand('John', '')); + } catch (err) { } + + const errors = logger.messages.filter(entry => entry.level === 'error'); + + expect(errors.length).to.be.greaterThan(0); + }); + }); From ac5fe7a0362f37a33c93b5ea5e629c5a7e697847 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Ko=C5=82ecki?= Date: Sat, 19 May 2018 23:00:56 +0200 Subject: [PATCH 5/9] Change messages to be private in FakeLogger and add getter --- test/component/commander.test.ts | 4 ++-- test/fakes/fakeLogger.ts | 6 +++++- test/unit/middlewares/commandLogger.test.ts | 8 ++++---- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/test/component/commander.test.ts b/test/component/commander.test.ts index 03b12a8..587f896 100644 --- a/test/component/commander.test.ts +++ b/test/component/commander.test.ts @@ -62,7 +62,7 @@ describe("Commander with middlewares combo", () => { it("should write info logs", async () => { await commander.handle(new HelloCommand('John', 'Doe')); - const infos = logger.messages.filter(entry => entry.level === 'info'); + const infos = logger.Messages.filter(entry => entry.level === 'info'); expect(infos.length).to.be.greaterThan(0); }); @@ -78,7 +78,7 @@ describe("Commander with middlewares combo", () => { await commander.handle(new HelloCommand('John', '')); } catch (err) { } - const errors = logger.messages.filter(entry => entry.level === 'error'); + const errors = logger.Messages.filter(entry => entry.level === 'error'); expect(errors.length).to.be.greaterThan(0); }); diff --git a/test/fakes/fakeLogger.ts b/test/fakes/fakeLogger.ts index da44e4e..96b7329 100644 --- a/test/fakes/fakeLogger.ts +++ b/test/fakes/fakeLogger.ts @@ -5,7 +5,11 @@ import { CommandLogger } from '../../src/middlewares'; export class FakeLogger implements CommandLogger { - public messages: { level: string, msg: string, meta: any }[] = []; + private messages: { level: string, msg: string, meta: any }[] = []; + + public get Messages() { + return this.messages; + } public log(level: string, msg: string, ...meta: any[]): CommandLogger { const placeholders: string[] = msg.match(/%[sdifjoO]/g) || []; diff --git a/test/unit/middlewares/commandLogger.test.ts b/test/unit/middlewares/commandLogger.test.ts index 413bf28..8bea07c 100644 --- a/test/unit/middlewares/commandLogger.test.ts +++ b/test/unit/middlewares/commandLogger.test.ts @@ -33,8 +33,8 @@ describe("Command Logger Middleware", () => { await commandLogger.run(new TestCommand(), next); - const infos = logger.messages.filter((entry) => entry.level === 'info'); - const errors = logger.messages.filter((entry) => entry.level === 'errors'); + const infos = logger.Messages.filter((entry) => entry.level === 'info'); + const errors = logger.Messages.filter((entry) => entry.level === 'errors'); expect(infos.length).to.be.equal(2); expect(errors.length).to.be.equal(0); @@ -75,8 +75,8 @@ describe("Command Logger Middleware", () => { await commandLogger.run(new TestCommand(), next); } catch (e) { } - const infos = logger.messages.filter((entry) => entry.level === 'info'); - const errors = logger.messages.filter((entry) => entry.level === 'error'); + const infos = logger.Messages.filter((entry) => entry.level === 'info'); + const errors = logger.Messages.filter((entry) => entry.level === 'error'); expect(infos.length).to.be.equal(2); expect(errors.length).to.be.equal(1); From 60d95df3f663ddeddd583bff9b43a3e9232e03c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Ko=C5=82ecki?= Date: Thu, 24 May 2018 00:21:16 +0200 Subject: [PATCH 6/9] Make command handler to return Promise --- src/commandBus.ts | 4 +- src/commander.ts | 6 +- src/middlewares/commandExecution.ts | 5 +- src/middlewares/commandLogger.ts | 2 +- src/middlewares/index.ts | 2 +- src/resolvers/memory.ts | 8 --- test/component/commander.test.ts | 55 +++++++++++++------ test/unit/commander.test.ts | 49 +++-------------- .../unit/middlewares/commandExecution.test.ts | 17 ++++-- test/unit/resolvers/memory.test.ts | 6 +- 10 files changed, 69 insertions(+), 85 deletions(-) diff --git a/src/commandBus.ts b/src/commandBus.ts index f886afb..ec8745d 100644 --- a/src/commandBus.ts +++ b/src/commandBus.ts @@ -2,9 +2,9 @@ export interface Command { } // marking interface export interface Handler { - handle(command: C): Promise; + handle(command: C): Promise; } export interface CommandBus { - handle(command: Command): Promise + handle(command: Command): Promise } diff --git a/src/commander.ts b/src/commander.ts index a6d6eb3..4942fff 100644 --- a/src/commander.ts +++ b/src/commander.ts @@ -11,8 +11,8 @@ export class Commander implements CommandBus { this.executionChain = this.createExecutionChain(middlewares); } - public async handle(command: Command): Promise { - return await this.executionChain(command); + public async handle(command: Command): Promise { + await this.executionChain(command); } private createExecutionChain(middlewares: Middleware[]): ExecutionChain { @@ -25,7 +25,7 @@ export class Commander implements CommandBus { next: ExecutionChain, middleware: Middleware ): ExecutionChain => { - return (command: Command): Promise => { + return (command: Command): Promise => { return middleware.run(command, () => next(command)); } }; diff --git a/src/middlewares/commandExecution.ts b/src/middlewares/commandExecution.ts index 926734a..f9abb06 100644 --- a/src/middlewares/commandExecution.ts +++ b/src/middlewares/commandExecution.ts @@ -8,10 +8,9 @@ export class CommandExecutionMiddleware implements Middleware { private handlerResolver: HandlerResolver ) { } - public async run(command: Command, next: NextMiddleware): Promise { + public async run(command: Command, next: NextMiddleware): Promise { const handler: Handler = this.handlerResolver.resolve(command); - const result = await handler.handle(command); + await handler.handle(command); await next(); - return result; } } diff --git a/src/middlewares/commandLogger.ts b/src/middlewares/commandLogger.ts index d7fd59c..89c0df7 100644 --- a/src/middlewares/commandLogger.ts +++ b/src/middlewares/commandLogger.ts @@ -25,7 +25,7 @@ export class CommandLoggerMiddleware implements Middleware { this.errorLevel = options.errorLevel || 'error'; } - public async run(command: Command, next: NextMiddleware): Promise { + public async run(command: Command, next: NextMiddleware): Promise { const start = Date.now(); const commandName = command.constructor.name; diff --git a/src/middlewares/index.ts b/src/middlewares/index.ts index 2e09040..676adab 100644 --- a/src/middlewares/index.ts +++ b/src/middlewares/index.ts @@ -4,7 +4,7 @@ import { Command } from '../commandBus'; export type NextMiddleware = () => Promise; export interface Middleware { - run(command: Command, next: NextMiddleware): Promise; + run(command: Command, next: NextMiddleware): Promise; } export * from './commandLogger'; diff --git a/src/resolvers/memory.ts b/src/resolvers/memory.ts index 44a523b..9bf4166 100644 --- a/src/resolvers/memory.ts +++ b/src/resolvers/memory.ts @@ -21,12 +21,4 @@ export class HandlerResolverInMemory implements HandlerResolver { return handler; } - - public count(): number { - return this.handlers.size; - } - - public entries(): IterableIterator<[ConstructorOf, Handler]> { - return this.handlers.entries(); - } } diff --git a/test/component/commander.test.ts b/test/component/commander.test.ts index 587f896..a756278 100644 --- a/test/component/commander.test.ts +++ b/test/component/commander.test.ts @@ -14,38 +14,50 @@ import { FakeLogger } from '../fakes/fakeLogger'; describe("Commander with middlewares combo", () => { - class HelloCommand implements Command { + type Person = {id: number, firstName: string, lastName: string}; + type PersonRepository = Person[]; + + class AddPersonCommand implements Command { constructor( public firstName: string, public lastName: string ) { } } - class HelloHandler implements Handler { - public wait(timeout: number): Promise { - return new Promise(resolve => setTimeout(resolve, timeout)); - } - - public async handle(hello: HelloCommand): Promise { - await this.wait(10); // simulate I/O waiting + class AddPersonHandler implements Handler { + private lastId = 123; - if (hello.lastName.length === 0) { - throw new Error('lastName is empty string'); + constructor( + private repository: any[] + ) {} + + public async handle(addPerson: AddPersonCommand): Promise { + if ( + addPerson.firstName.length === 0 || + addPerson.lastName.length === 0 + ) { + throw new Error('firstName and lastName cannot be empty string'); } - return `Hello, ${hello.firstName} ${hello.lastName}`; + this.repository.push({ + id: ++ this.lastId, + firstName: addPerson.firstName, + lastName: addPerson.lastName, + }); } } let logger: FakeLogger; let commander: Commander; let resolver: HandlerResolverInMemory; + let personRepository: PersonRepository; beforeEach(() => { + personRepository = []; logger = new FakeLogger(); resolver = new HandlerResolverInMemory(); - resolver.register(HelloCommand, new HelloHandler()); + resolver.register(AddPersonCommand, new AddPersonHandler(personRepository)); commander = new Commander([ new CommandLoggerMiddleware(logger), @@ -54,13 +66,17 @@ describe("Commander with middlewares combo", () => { }); it("should resolve with value from handler", async () => { - const promise = commander.handle(new HelloCommand('John', 'Doe')); + await commander.handle(new AddPersonCommand('John', 'Doe')); - return expect(promise).to.eventually.be.equal('Hello, John Doe'); + expect(personRepository).to.be.deep.equal([{ + id: 124, + firstName: 'John', + lastName: 'Doe', + }]); }); it("should write info logs", async () => { - await commander.handle(new HelloCommand('John', 'Doe')); + await commander.handle(new AddPersonCommand('John', 'Doe')); const infos = logger.Messages.filter(entry => entry.level === 'info'); @@ -68,14 +84,17 @@ describe("Commander with middlewares combo", () => { }); it("should reject when handler rejects", async () => { - const promise = commander.handle(new HelloCommand('John', '')); + const promise = commander.handle(new AddPersonCommand('John', '')); - return expect(promise).to.eventually.be.rejectedWith(Error, 'lastName is empty string'); + return expect(promise).to.eventually.be.rejectedWith( + Error, + 'firstName and lastName cannot be empty string' + ); }); it("should write error logs when handler rejects", async () => { try { - await commander.handle(new HelloCommand('John', '')); + await commander.handle(new AddPersonCommand('John', '')); } catch (err) { } const errors = logger.Messages.filter(entry => entry.level === 'error'); diff --git a/test/unit/commander.test.ts b/test/unit/commander.test.ts index c838623..ea6fd67 100644 --- a/test/unit/commander.test.ts +++ b/test/unit/commander.test.ts @@ -34,17 +34,7 @@ describe("Commander", () => { expect(result).to.be.undefined; }); - it("should resolve with value from Middleware", async () => { - const commander = new Commander([ - new CallbackMiddleware(async () => 'foo bar') - ]); - - const result = await commander.handle(testCommand); - - expect(result).to.be.equal('foo bar'); - }); - - it("should launch all middlewares in order of middleware next() call", async () => { + it("should resolve after launching all middlewares in order of middleware next() call", async () => { const createPusher = (array: any[], item: any) => { return async (command: Command, next: NextMiddleware) => { @@ -57,38 +47,15 @@ describe("Commander", () => { let counters: number[] = []; const commander = new Commander([ - new CallbackMiddleware(createPusher(counters, 0)), - new CallbackMiddleware(createPusher(counters, 1)), - new CallbackMiddleware(createPusher(counters, 2)), + new CallbackMiddleware(createPusher(counters, 'first')), + new CallbackMiddleware(createPusher(counters, 'second')), + new CallbackMiddleware(createPusher(counters, 'third')), ]); - await commander.handle(testCommand); + const promise = commander.handle(testCommand); - expect(counters).to.deep.equal([2, 1, 0]); + expect(counters.length).to.be.equal(0); + await promise; + expect(counters).to.be.deep.equal(['third', 'second', 'first']); }); - - it("should resolve value from last middleware if all middlewares return next()", async () => { - - const commander = new Commander([ - new CallbackMiddleware((command: Command, next: NextMiddleware) => { - return next(); - }), - new CallbackMiddleware((command: Command, next: NextMiddleware) => { - return next(); - }), - new CallbackMiddleware((command: Command, next: NextMiddleware) => { - return next(); - }), - - new CallbackMiddleware(async (command: Command, next: NextMiddleware) => { - await next(); - return 'foo bar'; - }), - ]); - - const result = await commander.handle(testCommand); - - expect(result).to.be.equal('foo bar'); - }); - }); diff --git a/test/unit/middlewares/commandExecution.test.ts b/test/unit/middlewares/commandExecution.test.ts index 4b06213..33552be 100644 --- a/test/unit/middlewares/commandExecution.test.ts +++ b/test/unit/middlewares/commandExecution.test.ts @@ -13,15 +13,23 @@ describe("Command Execution Middleware", () => { const testCommand = new class implements Command { }; - it("should return value from resolved handler", async () => { + it("should resolve after handler resolves", async () => { + const orderArr: string[] = []; const executor = new CommandExecutionMiddleware( - new FakeResolver(() => Promise.resolve('expected result')) + new FakeResolver(() => { + return Promise.resolve() + .then(() => orderArr.push('resolver')) + }) ); - const result = await executor.run(testCommand, noop); + await executor.run(testCommand, noop) + .then(() => orderArr.push('after executor')); - expect(result).to.equal('expected result'); + expect(orderArr).to.be.deep.equal([ + 'resolver', + 'after executor' + ]); }); it("should reject when handler rejects with the same reason", async () => { @@ -38,7 +46,6 @@ describe("Command Execution Middleware", () => { }); it("should resolve efter next() resolves", async () => { - const executor = new CommandExecutionMiddleware( new FakeResolver(() => Promise.resolve()) ); diff --git a/test/unit/resolvers/memory.test.ts b/test/unit/resolvers/memory.test.ts index 3d24051..ef75a21 100644 --- a/test/unit/resolvers/memory.test.ts +++ b/test/unit/resolvers/memory.test.ts @@ -12,13 +12,13 @@ describe("In Memory Handle Resolver", () => { class TestCommandWithNoHandler implements Command { } class FooHandler implements Handler { - handle(c: FooCommand): Promise { + handle(c: FooCommand): Promise { return Promise.resolve(); } } class BarHandler implements Handler { - handle(c: FooCommand): Promise { + handle(c: FooCommand): Promise { return Promise.resolve(); } } @@ -39,7 +39,7 @@ describe("In Memory Handle Resolver", () => { .to.throw(Error); }); - it("return resolve with registered handler", async () => { + it("resolve with registered handler", async () => { const givenHandler = new FooHandler(); resolver.register(FooCommand, givenHandler); resolver.register(BarCommand, new BarHandler()); From f5830d3d7808f1e829f854510c83569079cc3488 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Ko=C5=82ecki?= Date: Sun, 3 Jun 2018 23:22:17 +0200 Subject: [PATCH 7/9] Log command end only once on error --- src/middlewares/commandLogger.ts | 20 ++++++++++++++------ test/unit/middlewares/commandLogger.test.ts | 8 +++----- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/src/middlewares/commandLogger.ts b/src/middlewares/commandLogger.ts index 89c0df7..07da21d 100644 --- a/src/middlewares/commandLogger.ts +++ b/src/middlewares/commandLogger.ts @@ -14,6 +14,14 @@ export interface CommandLoggerOptions { errorLevel?: string; } +class Timer { + private start = Date.now(); + + public getTime(): number { + return Date.now() - this.start; + } +} + export class CommandLoggerMiddleware implements Middleware { private logger: CommandLogger; private level: string; @@ -26,19 +34,19 @@ export class CommandLoggerMiddleware implements Middleware { } public async run(command: Command, next: NextMiddleware): Promise { - const start = Date.now(); const commandName = command.constructor.name; this.logger.log(this.level, 'Command %s dispatched', commandName); + const timer = new Timer(); + try { - return await next(); + const res = await next(); + this.logger.log(this.level, 'Command %s succeeded (%dms)', commandName, timer.getTime()); + return res; } catch (err) { - this.logger.log(this.errorLevel, 'Command %s error. %s', commandName, err); + this.logger.log(this.errorLevel, 'Command %s failed (%dms): %s', commandName, timer.getTime(), err); throw err; - } finally { - const time = Date.now() - start; - this.logger.log(this.level, 'Command %s time %dms', commandName, time); } } } diff --git a/test/unit/middlewares/commandLogger.test.ts b/test/unit/middlewares/commandLogger.test.ts index 8bea07c..6974f98 100644 --- a/test/unit/middlewares/commandLogger.test.ts +++ b/test/unit/middlewares/commandLogger.test.ts @@ -40,7 +40,7 @@ describe("Command Logger Middleware", () => { expect(errors.length).to.be.equal(0); expect(infos[0].msg).to.equal("Command TestCommand dispatched"); - expect(infos[1].msg).to.equal("Command TestCommand time 25ms"); + expect(infos[1].msg).to.equal("Command TestCommand succeeded (25ms)"); }); it("should resolve with same value as next()", async () => { @@ -78,13 +78,11 @@ describe("Command Logger Middleware", () => { const infos = logger.Messages.filter((entry) => entry.level === 'info'); const errors = logger.Messages.filter((entry) => entry.level === 'error'); - expect(infos.length).to.be.equal(2); + expect(infos.length).to.be.equal(1); expect(errors.length).to.be.equal(1); expect(infos[0].msg).to.equal("Command TestCommand dispatched"); - expect(infos[1].msg).to.equal("Command TestCommand time 125ms"); - - expect(errors[0].msg).to.equal("Command TestCommand error. Error: foo bar"); + expect(errors[0].msg).to.equal("Command TestCommand failed (125ms): Error: foo bar"); }); }); From af6642be3da6edc059ebf4da753a212b1d9df35e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Ko=C5=82ecki?= Date: Sun, 3 Jun 2018 23:28:47 +0200 Subject: [PATCH 8/9] Make next() function to take command as argument --- src/commander.ts | 2 +- src/middlewares/commandExecution.ts | 2 +- src/middlewares/commandLogger.ts | 2 +- src/middlewares/index.ts | 2 +- test/unit/commander.test.ts | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/commander.ts b/src/commander.ts index 4942fff..0849bcd 100644 --- a/src/commander.ts +++ b/src/commander.ts @@ -26,7 +26,7 @@ export class Commander implements CommandBus { middleware: Middleware ): ExecutionChain => { return (command: Command): Promise => { - return middleware.run(command, () => next(command)); + return middleware.run(command, next); } }; diff --git a/src/middlewares/commandExecution.ts b/src/middlewares/commandExecution.ts index f9abb06..b34ab06 100644 --- a/src/middlewares/commandExecution.ts +++ b/src/middlewares/commandExecution.ts @@ -11,6 +11,6 @@ export class CommandExecutionMiddleware implements Middleware { public async run(command: Command, next: NextMiddleware): Promise { const handler: Handler = this.handlerResolver.resolve(command); await handler.handle(command); - await next(); + await next(command); } } diff --git a/src/middlewares/commandLogger.ts b/src/middlewares/commandLogger.ts index 07da21d..a0e5798 100644 --- a/src/middlewares/commandLogger.ts +++ b/src/middlewares/commandLogger.ts @@ -41,7 +41,7 @@ export class CommandLoggerMiddleware implements Middleware { const timer = new Timer(); try { - const res = await next(); + const res = await next(command); this.logger.log(this.level, 'Command %s succeeded (%dms)', commandName, timer.getTime()); return res; } catch (err) { diff --git a/src/middlewares/index.ts b/src/middlewares/index.ts index 676adab..0794e37 100644 --- a/src/middlewares/index.ts +++ b/src/middlewares/index.ts @@ -1,7 +1,7 @@ import { Command } from '../commandBus'; -export type NextMiddleware = () => Promise; +export type NextMiddleware = (command: Command) => Promise; export interface Middleware { run(command: Command, next: NextMiddleware): Promise; diff --git a/test/unit/commander.test.ts b/test/unit/commander.test.ts index ea6fd67..316a221 100644 --- a/test/unit/commander.test.ts +++ b/test/unit/commander.test.ts @@ -39,7 +39,7 @@ describe("Commander", () => { const createPusher = (array: any[], item: any) => { return async (command: Command, next: NextMiddleware) => { await Promise.resolve(true); - await next(); + await next(command); array.push(item); } } From 18f0bfd941e07a93422d7b9532fbd16d9523c385 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Ko=C5=82ecki?= Date: Wed, 6 Jun 2018 23:15:40 +0200 Subject: [PATCH 9/9] Add copyright info --- LICENSE | 1 + 1 file changed, 1 insertion(+) diff --git a/LICENSE b/LICENSE index 1101347..47897c2 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,7 @@ MIT License Copyright (c) 2017 Witold Wasiczko +Copyright (c) 2018 Mateusz KoĊ‚ecki Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal