Skip to content

Commit

Permalink
feat(re-implementation port): Re-implement existing functionality fro…
Browse files Browse the repository at this point in the history
…m simple-event-store

Tests - Event Data
Tests - Storage Event
Tests - Guard
Tests - Event Stream Appending
Tests - Event Stream Appending - Concurrency Checks
Tests - Event Stream Appending - Concurrency Check on existing stream
Tests - Event Stream Appending - Meta data integrity
Tests - Event Store Reading - Empty stream
Tests - Event Store Reading - Read full stream
Tests - Event Store Reading - Read partial stream
Tests - Event Store Reading - Invalid Stream Id
  • Loading branch information
Yannick Meeus authored and YannickMeeus committed Dec 13, 2018
1 parent d229f75 commit c2e9092
Show file tree
Hide file tree
Showing 17 changed files with 368 additions and 24 deletions.
9 changes: 9 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@
"statements": 95
}
},
"collectCoverage": true
"collectCoverage": false
},
"prettier": {
"semi": false,
Expand All @@ -79,6 +79,7 @@
"devDependencies": {
"@types/jest": "^22.0.0",
"@types/node": "^10.0.3",
"@types/uuid": "^3.4.3",
"colors": "^1.1.2",
"commitizen": "^2.9.6",
"coveralls": "^3.0.0",
Expand Down Expand Up @@ -106,6 +107,7 @@
"tslint-config-standard": "^7.0.0",
"typedoc": "^0.11.0",
"typescript": "^2.6.2",
"uuid": "^3.2.1",
"validate-commit-msg": "^2.12.2"
},
"dependencies": {}
Expand Down
8 changes: 4 additions & 4 deletions rollup.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,12 @@ export default {
input: `src/${libraryName}.ts`,
output: [
{ file: pkg.main, name: camelCase(libraryName), format: 'umd', sourcemap: true },
{ file: pkg.module, format: 'es', sourcemap: true },
{ file: pkg.module, format: 'es', sourcemap: true }
],
// Indicate here external modules you don't wanna include in your bundle (i.e.: 'lodash')
external: [],
watch: {
include: 'src/**',
include: 'src/**'
},
plugins: [
// Allow json resolution
Expand All @@ -33,6 +33,6 @@ export default {
resolve(),

// Resolve source maps to the original source
sourceMaps(),
],
sourceMaps()
]
}
13 changes: 13 additions & 0 deletions src/EventData.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { Guard } from './errors/Guard'

class EventData {
constructor(
public readonly eventId: string,
public readonly body: any,
public readonly metaData: any = undefined
) {
Guard.againstNull('body', body)
}
}

export { EventData }
5 changes: 4 additions & 1 deletion src/EventStore.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Guard } from './errors/Guard'
import { EventData, StorageEvent } from './StorageEvent'
import { StorageEvent } from './StorageEvent'
import { IStorageEngine } from './IStorageEngine'
import { EventData } from './EventData'

class EventStore {
constructor(private readonly engine: IStorageEngine) {
Expand Down Expand Up @@ -31,3 +32,5 @@ class EventStore {
return this.engine.readStreamForwards(streamId, startPosition, numberOfEvents)
}
}

export { EventStore }
23 changes: 11 additions & 12 deletions src/StorageEvent.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,20 @@
import { Guard } from './errors/Guard'
import { EventData } from './EventData'

// TODO: Rename as clashes with native type
class StorageEvent {
public eventBody: any
public metaData: any
public eventId: string

constructor(
public readonly streamId: string,
public readonly data: EventData,
eventData: EventData,
public readonly eventNumber: number
) {}
}

class EventData {
constructor(
public readonly eventId: string,
public readonly body: any,
public readonly metaData: any = undefined
) {
Guard.againstNull('body', body)
this.eventBody = eventData.body
this.metaData = eventData.metaData
this.eventId = eventData.eventId
}
}

export { StorageEvent, EventData }
export { StorageEvent }
7 changes: 7 additions & 0 deletions src/errors/ArgumentError.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
class ArgumentError extends Error {
constructor(message: string) {
super(message)
}
}

export { ArgumentError }
2 changes: 2 additions & 0 deletions src/errors/ConcurrencyError.ts
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
class ConcurrencyError extends Error {}

export { ConcurrencyError }
5 changes: 4 additions & 1 deletion src/errors/Guard.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import { NullableString } from '../types'
import { ArgumentError } from './ArgumentError'

class Guard {
public static againstNullOrEmpty(paramName: string, value: NullableString) {
if (this.isNull(value) || value!.trim().length === 0) {
throw new Error(`${paramName} can not be null, empty string or contain only whitespace`)
throw new ArgumentError(
`${paramName} can not be null, empty string or contain only whitespace`
)
}
}

Expand Down
17 changes: 12 additions & 5 deletions src/inMemory/InMemoryStorageEngine.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
import { IStorageEngine } from '../IStorageEngine'
import { StorageEvent } from '../StorageEvent'
import { ConcurrencyError } from '../errors/ConcurrencyError'

class InMemoryStorageEngine implements IStorageEngine {
constructor(private readonly streams: Map<string, StorageEvent[]>) {}
private readonly streams: Map<string, StorageEvent[]>

constructor() {
this.streams = new Map<string, StorageEvent[]>()
}

public async appendToStream(streamId: string, events: StorageEvent[]): Promise<void> {
if (!this.streams.has(streamId)) {
Expand All @@ -17,8 +22,8 @@ class InMemoryStorageEngine implements IStorageEngine {
} : Actual revision ${this.streams.get(streamId)!.length}"`
)
}

this.streams.set(streamId, events)
const stream = this.streams.get(streamId)!.concat(events)
this.streams.set(streamId, stream)
}

public async readStreamForwards(
Expand All @@ -29,11 +34,13 @@ class InMemoryStorageEngine implements IStorageEngine {
if (!this.streams.has(streamId)) {
return []
}

return this.streams.get(streamId)!.slice(startPosition, startPosition + numberOfEvents)
const index = startPosition <= 0 ? 0 : startPosition - 1
return this.streams.get(streamId)!.slice(index, startPosition + numberOfEvents)
}

public async initialise(): Promise<IStorageEngine> {
return this
}
}

export { InMemoryStorageEngine }
25 changes: 25 additions & 0 deletions test/EventData.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { EventData } from '../src/EventData'
import * as uuid from 'uuid'

describe('When the body is not provided', () => {
it('It should throw an error', () => {
expect(() => new EventData(uuid.v4(), undefined)).toThrowError('body can not be null')
})
})
describe('When creating an instance of an EventData object', () => {
const expectedEventId = uuid.v4()
const expectedMetaData = 'METADATA'
const expectedBody = 'BODY'

const eventData = new EventData(expectedEventId, expectedBody, expectedMetaData)

it('It should map the event Id correctly', () => {
expect(eventData.eventId).toEqual(expectedEventId)
})
it('It should map the body correctly', () => {
expect(eventData.body).toEqual(expectedBody)
})
it('It should map the meta data correctly', () => {
expect(eventData.metaData).toEqual(expectedMetaData)
})
})
144 changes: 144 additions & 0 deletions test/EventStore.Appending.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
import { InMemoryStorageEngine } from '../src/inMemory/InMemoryStorageEngine'
import { EventStore } from '../src/EventStore'
import { IStorageEngine } from '../src/IStorageEngine'
import { StorageEvent } from '../src/StorageEvent'
import * as uuid from 'uuid'
import { EventData } from '../src/EventData'
import { OrderCreated } from './Events/OrderCreated'
import { OrderDispatched } from './Events/OrderDispatched'

describe('Given a set of engines to test against', () => {
const engineFactories: (() => IStorageEngine)[] = [() => new InMemoryStorageEngine()]

const newGuid = () => uuid.v4()

engineFactories.forEach(getEngine => {
const engine = getEngine()

const getStore = async () => {
await engine.initialise()
return new EventStore(engine)
}
describe('When appending to a new stream', () => {
describe('And the stream id is invalid', () => {
const invalidStreamIds = [undefined, null, '', ' ']
invalidStreamIds.forEach(invalidStreamId => {
it(`It should throw an error for stream id: '${invalidStreamId}'`, async () => {
const eventStore = await getStore()
const event = new EventData(newGuid(), 'BODY')
try {
await eventStore.AppendToStream(invalidStreamId as string, 0, event)
} catch (e) {
expect(e.message).toEqual(
'streamId can not be null, empty string or contain only whitespace'
)
}
})
})
})
describe('And we have multiple events to save', () => {
const streamId = newGuid()
const firstEvent = new EventData(newGuid(), new OrderCreated(streamId))
const secondEvent = new EventData(newGuid(), new OrderDispatched(streamId))
const eventsToSave = [firstEvent, secondEvent]

it('It should save both events and allow them to be retrievable', async () => {
const sut = await getStore()
await sut.AppendToStream(streamId, 0, ...eventsToSave)

const savedEvents = await sut.readStreamForwards(streamId)

expect(savedEvents.length).toEqual(2)
const firstSavedEvent = savedEvents.shift() as StorageEvent
const secondSavedEvent = savedEvents.shift() as StorageEvent

expect(firstSavedEvent.streamId).toEqual(streamId)
expect(firstSavedEvent.eventNumber).toEqual(1)

expect(secondSavedEvent.streamId).toEqual(streamId)
expect(secondSavedEvent.eventNumber).toEqual(2)
})
})
it('It should save the event', async () => {
const streamId = newGuid()
const sut = await getStore()
const event = new EventData(newGuid(), new OrderCreated(streamId))

await sut.AppendToStream(streamId, 0, event)

const stream = await sut.readStreamForwards(streamId)
expect(stream.length).toEqual(1)
const savedEvent = stream[0]
expect(savedEvent.streamId).toEqual(streamId)
expect(savedEvent.eventId).toEqual(event.eventId)
expect(savedEvent.eventNumber).toEqual(1)
})
it('It should save the meta data correctly', async () => {
interface SomeMetaData {
value: string
}

const metaData: SomeMetaData = {
value: 'foo'
}

const streamId = newGuid()
const sut = await getStore()
const event = new EventData(newGuid(), new OrderCreated(streamId), metaData)

await sut.AppendToStream(streamId, 0, event)
const stream = await sut.readStreamForwards(streamId)
const savedEvent = stream.pop() as StorageEvent
expect(savedEvent.metaData as SomeMetaData).toEqual(metaData)
})
})
describe('When appending to an existing stream', () => {
it('It should save the event', async () => {
const streamId = newGuid()
const sut = await getStore()
const firstEvent = new EventData(newGuid(), new OrderCreated(streamId))
const secondEvent = new EventData(newGuid(), new OrderDispatched(streamId))
await sut.AppendToStream(streamId, 0, firstEvent)

await sut.AppendToStream(streamId, 1, secondEvent)

const stream = await sut.readStreamForwards(streamId)

expect(stream.length).toEqual(2)
const lastEvent = stream.pop() as StorageEvent
expect(lastEvent.eventId).toEqual(secondEvent.eventId)
expect(lastEvent.eventNumber).toEqual(2)
})
})
describe('When appending to a new stream with an unexpected version', () => {
const invalidRevisions = [-1, 1, 2, 99]
invalidRevisions.forEach(invalidRevision => {
it(`It should throw a concurrency error with revision number: '${invalidRevision}'`, async () => {
const streamId = newGuid()
const sut = await getStore()
const event = new EventData(newGuid(), new OrderDispatched(streamId))

await expect(sut.AppendToStream(streamId, invalidRevision, event)).rejects.toThrow(
'Concurrency conflict'
)
})
})
})
describe('When appending to an existing stream with an unexpected version', () => {
const invalidRevisions = [0, 2]
invalidRevisions.forEach(invalidRevision => {
it(`It should throw a concurrency error with revision number: '${invalidRevision}'`, async () => {
const streamId = newGuid()
const sut = await getStore()

const existingEvent = new EventData(newGuid(), new OrderCreated(streamId))
const newEvent = new EventData(newGuid(), new OrderDispatched(streamId))
await sut.AppendToStream(streamId, 0, existingEvent)
await expect(sut.AppendToStream(streamId, invalidRevision, newEvent)).rejects.toThrow(
'Concurrency conflict'
)
})
})
})
})
})
Loading

0 comments on commit c2e9092

Please sign in to comment.