Skip to content

Commit

Permalink
Merge branch 'master' of github.com:wesone/blackrik
Browse files Browse the repository at this point in the history
  • Loading branch information
hvipana committed Oct 14, 2022
2 parents 86d96e5 + cbea8f5 commit 5f3b9d0
Show file tree
Hide file tree
Showing 62 changed files with 1,017 additions and 798 deletions.
26 changes: 18 additions & 8 deletions .eslintrc.js
Expand Up @@ -75,13 +75,13 @@ module.exports = {
{
'after': true,
'overrides': {
'catch': { 'after': false },
'do': { 'after': false },
'for': { 'after': false },
'function': { 'after': false },
'if': { 'after': false },
'while': { 'after': false },
'switch': { 'after': false }
'catch': {'after': false},
'do': {'after': false},
'for': {'after': false},
'function': {'after': false},
'if': {'after': false},
'while': {'after': false},
'switch': {'after': false}
}
}
],
Expand Down Expand Up @@ -136,6 +136,14 @@ module.exports = {
'avoidQuotes': true
}
],
'object-curly-spacing': [
'error',
'never'
],
'array-bracket-spacing': [
'error',
'never'
],
'no-whitespace-before-property': [
'error'
],
Expand All @@ -155,9 +163,11 @@ module.exports = {
'no-lonely-if': [
'error'
],
'no-multi-spaces': 'warn',
'eol-last': [
'error',
'always'
]
],
'no-template-curly-in-string': 'off'
}
};
12 changes: 11 additions & 1 deletion CHANGELOG.md
Expand Up @@ -5,6 +5,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

## [1.2.1] - 2022-09-06
### Fixed
- Fixed possibility of command scheduling failure due to invalid database type

## [1.2.0] - 2022-08-18
### Changed
- Return values of a command handler that do not have a `type` property will be ignored

Expand All @@ -13,6 +19,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added
- Filtering duplicate events that will be replayed on startup to improve performance
- Emitting a `TOMBSTONE` event will delete the whole aggregate
- Blackrik function `deleteAggregate` (also available in side effects)

## [1.1.3] - 2021-08-05
### Changed
Expand Down Expand Up @@ -88,7 +96,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
- Initial release

[Unreleased]: https://github.com/wesone/blackrik/compare/v1.1.3...HEAD
[Unreleased]: https://github.com/wesone/blackrik/compare/v1.2.1...HEAD
[1.2.1]: https://github.com/wesone/blackrik/compare/v1.2.0...v1.2.1
[1.2.0]: https://github.com/wesone/blackrik/compare/v1.1.3...v1.2.0
[1.1.3]: https://github.com/wesone/blackrik/compare/v1.1.2...v1.1.3
[1.1.2]: https://github.com/wesone/blackrik/compare/v1.1.1...v1.1.2
[1.1.1]: https://github.com/wesone/blackrik/compare/v1.1.0...v1.1.1
Expand Down
42 changes: 28 additions & 14 deletions docs/Aggregates.md
Expand Up @@ -4,8 +4,8 @@ It consists of a name, [commands](#Commands) and a [projection](#Projection).
A specific instance of an aggregate (e.g. an actual user) is identified by `aggregateId`.

# Commands
A command is a request to change/update an aggregates state (e.g. update the users name).
It can be triggered through the [HTTP API](HTTP-API#Commands) or locally through [executeCommand](Blackrik#executeCommand).
A command is a request to change/update an aggregate's state (e.g. update the user's name).
It can be triggered through the [HTTP API](HTTP-API#Commands) or programmatically through [executeCommand](Blackrik#executeCommand).

## Callback
The actual callback that will be executed will receive 3 parameters.
Expand All @@ -15,7 +15,7 @@ To reject a command (e.g. in case the validation of the payload failed) the call
Name | Type | Attribute | Description
:--- | :--- | :--- | :---
command | object | | The [command](#Command) to be processed by the callback
state | object | | The calculated state that is created with the help of the aggregates [projection](#Projection) and that is based on all events that belong to the aggregateId
state | object | | The calculated state that is created with the help of the aggregate's [projection](#Projection) and that is based on all events that belong to the aggregate id
context | object | | An object that contains [additional information or helper functions](#Context)

### Return
Expand All @@ -27,10 +27,10 @@ The command object contains the following properties:
Property | Type | Attribute | Description
:--- | :--- | :--- | :---
aggregateName | string | | The name of the aggregate
aggregateId | string | | The aggregateId to identify an aggregate instance
aggregateId | string | | The aggregate id to identify an aggregate instance
type | string | | The name of the command to execute
timestamp | int | | The timestamp of the command issuing
payload | object | optional<br>default: `null` | An optional payload that contains options or arguments for the command
payload | mixed | optional<br>default: `null` | An optional payload that contains options or arguments for the command

## Context
The context is an object that contains additional information or functions.
Expand All @@ -43,14 +43,17 @@ latestEventPosition | number \| null | `null` <small>for a new aggregate id</sma
causationEvent | object | optional | An event that caused this command. If the command returns a new event, the causationEvent will be the events "parent"

## Event
The object that may be returned by the commands callback initiates a new event.
That event will automatically be assigned to the aggregateId and will be populated with additional information (timestamp, aggregateVersion, ...)
The object that may be returned by the command's callback initiates a new event.
That event will automatically be assigned to the aggregate id and will be populated with additional information (timestamp, aggregateVersion, ...)

Property | Type | Attribute | Description
:--- | :--- | :--- | :---
type | string | | The type of event
payload | object | optional<br>default: `null` | An optional payload that contains additional information for the event

### Tombstone event
There is a special event called *Tombstone event*. If a command returns an event with the type `TOMBSTONE`, it will automatically erase all events inside the event store that belong to the aggregate id of this event. [More information on why this exists](SensitiveData).

## Examples
```javascript
module.exports = {
Expand All @@ -70,25 +73,36 @@ module.exports = {
payload
};
},
update: async ({payload}, state, context) => {
if(payload.email)
throw new BadRequestError('You can not update your email address');
updateName: async ({payload}, state, context) => {
if(!state.registered)
throw new NotFoundError();
if(!payload.name)
throw new BadRequestError('Please give us your real name');
if(payload.name === state.name)
throw new BadRequestError('There are no changes');
throw new UnalteredError('There are no changes');

return {
type: 'USER_UPDATED',
type: 'USER_NAME_UPDATED',
payload
};
},
requestGDPRDeletion: async (command, state, context) => {
if(!state.registered)
throw new NotFoundError();

return {
type: 'TOMBSTONE',
payload: {
reason: 'GDPR request'
}
}
}
};
```

# Projection
The projection will reduce all events that belong to the aggregateId and the resulting state will be passed to the commands callback.
To achieve this, the projection contains a function for each event type of the aggregate.
The projection will reduce all events that belong to the aggregate id and the resulting state will be passed to the command's callback.
To achieve this, the projection contains a function for each event type of the aggregate (that actually affects the state).
The function will receive the previous state and the event and returns the new state.
A projection can also have a function called `init` that returns a starting state. Without an init-function the first state will be an empty object (`{}`).

Expand Down
34 changes: 33 additions & 1 deletion docs/Blackrik.md
Expand Up @@ -10,6 +10,7 @@ public | async [stop](Blackrik#stop)()<br>Stops the application
public | async [executeCommand](Blackrik#executeCommand)(command: object): boolean<br>Calls a command
public | async [scheduleCommand](Blackrik#scheduleCommand)(timestamp: number, command: object): boolean<br>Delays a command execution
public | async [executeQuery](Blackrik#executeQuery)(readModel: string, resolver: string, ?query: object): mixed<br>Performs a query on a resolver
public | async [deleteAggregate](Blackrik#deleteAggregate)(aggregateName: string, aggregateId: string, ?eventPayload: mixed): boolean<br>Deletes the specified aggregate and emits a Tombstone event

# ADAPTERS
An object containing the default adapters that can be used inside read model store adapters, event store adapter, event bus adapter.
Expand Down Expand Up @@ -85,11 +86,13 @@ UnauthorizedError // 401 Unauthorized
ForbiddenError // 403 Forbidden
NotFoundError // 404 Not Found
ConflictError // 409 Conflict
DuplicateAggregateError // 409 Aggregate already created
UnalteredError // 400 Unaltered state
```

### Examples
```javascript
const {BadRequestError, BaseError} = require('blackrik').ERRORS
const {BadRequestError, BaseError} = require('blackrik').ERRORS;

if(!isValidRequest())
throw new BadRequestError();
Expand Down Expand Up @@ -209,3 +212,32 @@ May throw an error
```javascript
const response = await executeQuery('users', 'get', {id: 42});
```

# deleteAggregate
`async deleteAggregate(aggregateName: string, aggregateId: string, ?eventPayload: mixed): boolean`
Deletes the specified aggregate and emits a Tombstone event with an optional payload. [More information on why this exists](SensitiveData).

### Parameters
Name | Type | Attribute | Description
:--- | :--- | :--- | :---
aggregateName | string | | The name of the aggregate
aggregateId | string | | The aggregate id to identify an aggregate instance
eventPayload | object | optional<br>default: `null` | An optional payload for the resulting Tombstone event

### Return
`true` if the deletion was successful, otherwise `false`
May throw an error

### Examples
```javascript
// inside a saga
'USER_GDPR_DELETION_REQUESTED': async (store, {aggregateId}, sideEffects) => {
await sideEffects.deleteAggregate('User', aggregateId, {reason: 'GDPR'});
}

// inside the read model projection
'TOMBSTONE': async (store, {aggregateId: id, payload: {reason}}) => {
if(reason === 'GDPR')
await store.delete('Users', {id});
}
```
19 changes: 19 additions & 0 deletions docs/EventStoreAdapter.md
Expand Up @@ -9,6 +9,7 @@ Visibility | Property
public | async [init](EventStoreAdapter#init)()<br>Initializes the adapter
public | async [save](EventStoreAdapter#save)(event: object): int \| false<br>Saves an event
public | async [load](EventStoreAdapter#load)(filter: object): object<br>Loads events
public | async [delete](EventStoreAdapter#delete)(aggregateId: string): number<br>Removes every event of the specified aggregate id
public | async [close](EventStoreAdapter#close)()<br>Closes the event store

# init
Expand Down Expand Up @@ -62,6 +63,7 @@ causationIds | array | optional | An array of strings with the desired causation
since | int | optional | To exclude events with a `timestamp` < `since`
until | int | optional | To exclude events with a `timestamp` >= `until`
limit | int | optional | To limit the number of returned events
reverse | boolean | optional<br>default: `false` | To reverse the order of the events
cursor | mixed \| null | optional | A cursor to scroll the result if a limit was used

### Return
Expand Down Expand Up @@ -107,6 +109,23 @@ const {events, cursor} = await eventStore.load({
});
```

# delete
`async delete(aggregateId: string): number`
Deletes all events that belong to the specified aggregate id.

### Parameters
Name | Type | Attribute | Description
:--- | :--- | :--- | :---
aggregateId | string | | The aggregate id

### Return
The `amount` (number) of events that were deleted.

### Examples
```javascript
const eventCount = await eventStore.delete('4233fb22-76eb-4b8f-9b34-8fdcb994e370');
```

# close
`async close()`
Securely closes all open connections to the database server.
2 changes: 1 addition & 1 deletion docs/HTTP-API.md
Expand Up @@ -21,7 +21,7 @@ The request body contains the command to execute as json.
Property | Type | Attribute | Description
:--- | :--- | :--- | :---
aggregateName | string | <small>can be specified inside the URL</small> | The name of the aggregate
aggregateId | string | | The aggregateId to identify an aggregate instance<br>The client may generate a new aggregateId if the desired aggregate does not exist yet
aggregateId | string | | The aggregate id to identify an aggregate instance<br>The client may generate a new aggregateId if the desired aggregate does not exist yet
type | string | <small>can be specified inside the URL</small> | The name of the command to execute
payload | object | optional | An optional payload that contains options or arguments for the command

Expand Down
1 change: 1 addition & 0 deletions docs/Sagas.md
Expand Up @@ -65,6 +65,7 @@ Property | Type | Attribute | Description
:--- | :--- | :--- | :---
[executeCommand](Blackrik#executeCommand) | function | | Calls a command
[scheduleCommand](Blackrik#scheduleCommand) | function | | Delays a command execution
[deleteAggregate](Blackrik#deleteAggregate) | function | | Deletes an aggregate and emits a Tombstone event

Events that will be created by using the default side effects will be linked to the event that caused them.
For example:
Expand Down
39 changes: 39 additions & 0 deletions docs/SensitiveData.md
@@ -0,0 +1,39 @@
# Handling the General Data Protection Regulation (GDPR)
To comply with the *General Data Protection Regulation (GDPR)* it is mandatory to handle sensitive data securely and prevent unauthorized access.
It is also a requirement to delete personal data (data that can be used to reveal a person's identity) if requested.

One way to handle this deletion can be anonymization. For example one could replace a person's full name with **John Doe**.
But how can we handle this in a world with an append-only event store?

Updating the name to **John Doe** would just lead to a new event. However the old event with the original name still exists inside the event store.
A few solutions to this problem already exist out there and all of them can be implemented with Blackrik.

## Referencing
We keep personal data out of our events and just store references to another system that holds the actual information.
If an event needs to contain personal data, we would write that data into the other system and just write a reference into the event.
A read model would then use that reference to get the personal data from the other system and maintain it's own database.

If a deletion is requested, we can delete the personal data from the other system without affecting our events in any way.
Our application just needs to handle the case that a reference from an event does not exist anymore inside the other system.
And we would also need a mechanism to erase the personal data from the read model store.

## Crypto-shredding
All personal data will be encrypted before it gets written into an event.
We would need at least one private key for each aggregate id and we would build a key management system to keep track of all the keys (which aggregate id has which private key).
A read model would then decrypt the data from an event with the help of the key management system to maintain it's own database.

If a deletion is required, we can simply delete the affected private key(s) from the key management system without affecting our events in any way.
This means we would not delete the data but just prevent access to it. Using this method requires us to use a strong encryption algorithm to make sure it is "impossible" to decrypt the data without the key. However keep in mind that a strong encryption algorithm today may be a weak one in a few years.
We would also need a mechanism to erase the personal data from the read model store.

## Deleting
You may have learned that individual events are generally considered immutable. The event store after all is an append-only database. Manipulating or deleting events is something you should not do because an event reflects something that happened and the past cannot be changed.

But what if we delete all events of a specific aggregate id?
As an aggregate is an encapsuled unit, deleting that unit would not affect any other aggregates.
So deleting whole aggregates from the event store and pretending that they never existed should not lead to problems.
The advantage is that there is not much overhead. You wouldn't need additional systems or databases.

With Blackrik you can return an event with the type [`TOMBSTONE` from a command](Aggregates#tombstone-event) or you can call [deleteAggregate](Blackrik#deleteAggregate) from an API handler or as [side effect](Sagas#sideeffects) inside a saga.
This will erase all affected events from the event store and the read model projection can listen to the Tombstone event to clear it's database accordingly.
Keep in mind that if you created backups of your event store you would have to tidy up these backups too.
2 changes: 2 additions & 0 deletions docs/_sidebar.md
Expand Up @@ -12,6 +12,8 @@
* [EventStoreAdapter](EventStoreAdapter)
* [EventBusAdapter](EventBusAdapter)
* [HTTP API](HTTP-API)
* Concepts
* [Sensitive data](SensitiveData)
* Contributing
* [Guide](CONTRIBUTING)
* [Issues](https://github.com/wesone/blackrik/issues)
Expand Down

0 comments on commit 5f3b9d0

Please sign in to comment.