Skip to content

Commit 1f5eac0

Browse files
committed
Major overhaul
1 parent e3b15a3 commit 1f5eac0

17 files changed

+349
-380
lines changed

Specification.md

Lines changed: 85 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -1,72 +1,84 @@
1-
## EventStore
1+
# EventStore
22

3-
### Reading
3+
## Reading
44

55
Expects a [StreamQuery](#StreamQuery) and an optional starting [SequenceNumber](#SequenceNumber) and returns an [EventStream](#EventStream).
66

7-
**Note:** The EventStore should also allow for _backwards_ iteration on the EventStream in order to support cursor based pagination.
7+
> [!NOTE]
8+
> The EventStore should also allow for _backwards_ iteration on the EventStream in order to support cursor based pagination.
89
9-
### Writing
10+
## Writing
1011

1112
Expects a set of [Events](#Events) and an [AppendCondition](#AppendCondition) and returns the last appended [SequenceNumber](#SequenceNumber).
1213

13-
#### Potential API
14+
## API
15+
16+
A potential interface of the `EventStore` (pseudo-code):
1417

1518
```
1619
EventStore {
20+
read(query: StreamQuery, options?: ReadOptions): EventStream
21+
append(events: Events|Event, condition: AppendCondition): SequenceNumber
22+
}
23+
```
1724

18-
read(query: StreamQuery, from?: SequenceNumber): EventStream
19-
20-
readBackwards(query: StreamQuery, from?: SequenceNumber): EventStream
21-
22-
append(events: Events, condition: AppendCondition): SequenceNumber
23-
25+
### ReadOptions
26+
27+
An optional parameter to `EventStore.read()` that allows for cursor-based pagination of events.
28+
It has two parameters:
29+
- `backwards` a flag that, if set to `true`, returns the events in descending order (default: `false`)
30+
- `from` an optional [SequenceNumber](#SequenceNumber) to start streaming events from (depending on the `backwards` flag this is either a _minimum_ or _maximum_ sequence number of the resulting stream)
31+
32+
```
33+
ReadOptions {
34+
from?: SequenceNumber
35+
backwards: bool
2436
}
2537
```
2638

27-
## StreamQuery
39+
### StreamQuery
2840

29-
The `StreamQuery` describes constraints that must be matched by [Event](#Event)s in the [EventStore](#EventStore).
41+
The `StreamQuery` describes constraints that must be matched by [Event](#Event)s in the [EventStore](#EventStore)
42+
It effectively allows for filtering events by their [type](#EventType) and/or [tags](#Tags)
3043

31-
* It _MAY_ contain a set of [StreamQuery Criteria](#StreamQuery-Criterion)
44+
* It _MAY_ contain a set of [StreamQuery Criteria](#StreamQuery-Criterion) – a `StreamQuery` with an empty criteria set is considered a "wildcard" query, i.e. it matches all events
3245

33-
**Note:** All criteria of a StreamQuery are merged into a *logical disjunction*, so the example below matches all events, that match the first **OR** the second criterion.
46+
> [!NOTE]
47+
> All criteria of a StreamQuery are merged into a *logical disjunction*, so events match the query if they match the first **OR** the second criterion...
3448
35-
#### Potential serialization format
49+
### StreamQuery Criterion
3650

37-
```json
38-
{
39-
"version": "1.0",
40-
"criteria": [{
41-
"type": "EventTypes",
42-
"properties": {
43-
"event_types": ["EventType1", "EventType2"]
44-
}
45-
}, {
46-
"type": "Tags",
47-
"properties": {
48-
"tags": ["foo:bar", "baz:foos"],
49-
}
50-
}, {
51-
"type": "EventTypesAndTags",
52-
"properties": {
53-
"event_types": ["EventType2", "EventType3"],
54-
"tags": ["foo:bar", "foo:baz"],
55-
}
56-
}]
57-
}
58-
```
51+
Each criterion of a [StreamQuery](#StreamQuery) allows to target events by their [type](#EventType) and/or [tags](#Tags)
5952

53+
> [!NOTE]
54+
> event type filters of a single criterion are merged into a *logical disjunction*, so events match the criterion if they match **ANY** of the specified types
55+
> tags are merged into a *logical conjunction*, so events match the criterion if they are tagged with **ALL** specified tags
6056
61-
## StreamQuery Criterion
57+
#### Example StreamQuery
6258

63-
In v1 the only supported criteria types are:
59+
The following example query would match events that are either...
60+
- ...of type `EventType1` **OR** `EventType2`
61+
- ...tagged `foo:bar` **AND** `baz:foos`
62+
- ...of type `EventType2` **OR** `EventType3` **AND** tagged `foo:bar`**AND** `foo:baz`
6463

65-
* `Tags` – allows to target one or more [Tags](#Tags)
66-
* `EventTypes` – allows to target one or more [EventType](#EventType)s
67-
* `EventTypesAndTags` – allows to target one or more [Tags](#Tags) and one or more [EventType](#EventType)s
64+
```json
65+
{
66+
"criteria": [
67+
{
68+
"event_types": ["EventType1", "EventType2"]
69+
},
70+
{
71+
"tags": ["foo:bar", "baz:foos"]
72+
},
73+
{
74+
"event_types": ["EventType2", "EventType3"],
75+
"tags": ["foo:bar", "foo:baz"]
76+
}
77+
]
78+
}
79+
```
6880

69-
## SequenceNumber
81+
### SequenceNumber
7082

7183
When an [Event](#Event) is appended to the [EventStore](#EventStore) a `SequenceNumber` is assigned to it.
7284

@@ -77,8 +89,7 @@ It...
7789
* _CAN_ contain gaps
7890
* _SHOULD_ have a reasonably high maximum value (depending on programming language and environment)
7991

80-
81-
## EventStream
92+
### EventStream
8293

8394
When reading from the [EventStore](#EventStore) an `EventStream` is returned.
8495

@@ -89,94 +100,92 @@ It...
89100
* Individual [EventEnvelope](#EventEnvelope) instances _MAY_ be converted during iteration for performance optimization
90101
* Batches of events _MAY_ be loaded from the underlying storage at once for performance optimization
91102

92-
## EventEnvelope
103+
### EventEnvelope
93104

94105
Each item in the [EventStream](#EventStream) is an `EventEnvelope` that consists of the underlying event and metadata, like the [SequenceNumber](#SequenceNumber) that was added during the `append()` call.
95106

107+
It...
108+
* It _MUST_ contain the [SequenceNumber](#SequenceNumber)
109+
* It _MUST_ contain the [Event](#Event)
110+
* It _CAN_ include more fields, like timestamps or metadata
111+
112+
#### EventEnvelope example
113+
96114
```json
97115
{
98116
"event": {
99-
"id": "15aaa216-4179-46d9-999a-75516e21a1c6",
100117
"type": "SomeEventType",
101-
"data": "{\"some\":\"data\"}"
118+
"data": "{\"some\":\"data\"}",
102119
"tags": ["type1:value1", "type2:value2"]
103120
},
104-
"sequence_number": 1234
121+
"sequence_number": 1234,
122+
"recorded_at": "2024-12-10 14:02:40"
105123
}
106124
```
107125

108-
## Events
126+
### Events
109127

110128
A set of [Event](#Event) instances that is passed to the `append()` method of the [EventStore](#EventStore)
111129

112130
It...
113131
* _MUST_ not be empty
114132
* _MUST_ be iterable, each iteration returning an [Event](#Event)
115133

116-
## Event
134+
### Event
117135

118-
* It _MUST_ contain a globally unique [EventId](#EventId)
119136
* It _MUST_ contain an [EventType](#EventType)
120137
* It _MUST_ contain [EventData](#EventData)
121138
* It _MAY_ contain [Tags](#Tags)
139+
* It _MAY_ contain further fields, like metadata
122140

123141
#### Potential serialization format
124142

125143
```json
126144
{
127-
"id": "15aaa216-4179-46d9-999a-75516e21a1c6",
128145
"type": "SomeEventType",
129-
"data": "{\"some\":\"data\"}"
146+
"data": "{\"some\":\"data\"}",
130147
"tags": ["key1:value1", "key1:value2"]
131148
}
132149
```
133150

134-
## EventId
135-
136-
String based globally unique identifier of an [Event](#Event)
137-
138-
* It _MUST_ satisfy the regular expression `^[\w\-]{1,100}$`
139-
* It _MAY_ be implemented as a [UUID](https://www.ietf.org/rfc/rfc4122.txt)
140-
141-
## EventType
151+
### EventType
142152

143-
String based type of an event
153+
String based type of the event
144154

145155
* It _MUST_ satisfy the regular expression `^[\w\.\:\-]{1,200}$`
146156

147-
## EventData
157+
### EventData
148158

149159
String based, opaque payload of an [Event](#Event)
150160

151161
* It _SHOULD_ have a reasonable large enough maximum length (depending on language and environment)
152162
* It _MAY_ contain [JSON](https://www.json.org/)
153163
* It _MAY_ be serialized into an empty string
154164

155-
## Tags
165+
### Tags
156166

157167
A set of [Tag](#Tag) instances.
158168

159169
* It _MUST_ contain at least one [Tag](#Tag)
160-
* It _MAY_ contain multiple [Tag](#Tag)s with the same value
161-
* It _SHOULD_ not contain muliple [Tag](#Tag)s with the same key/value pair
170+
* It _SHOULD_ not contain multiple [Tag](#Tag)s with the same value
162171

163-
## Tag
172+
### Tag
164173

165-
A `Tag` can add domain specific metadata (usually the ids of an entity or concept of the core domain) to an event allowing for custom partitioning
174+
A `Tag` can add domain specific metadata to an event allowing for custom partitioning
166175

167-
**NOTE:** If the `value` is not specified, all tags of the given `key` will match (wildcard)
176+
> [!NOTE]
177+
> Usually a tag represents a concept of the domain, e.g. the type and id of an entity like `product:p123`
168178
169-
* It _MUST_ contain a `key` that satisfies the regular expression `^[a-zA-Z0-9\-\_]{1,50}$`
170-
* It _CAN_ contain a `value` that satisfies the regular expression `^[a-zA-Z0-9\-\_]{1,50}$`
179+
* It _MUST_ satisfy the regular expression `/^[[:alnum:]\-\_\:]{1,150}`
171180

172-
## AppendCondition
181+
### AppendCondition
173182

174183
* It _MUST_ contain a [StreamQuery](#StreamQuery)
175184
* It _MUST_ contain a [ExpectedHighestSequenceNumber](#ExpectedHighestSequenceNumber)
176185

177-
## ExpectedHighestSequenceNumber
186+
### ExpectedHighestSequenceNumber
178187

179-
Can _either_ be an instance of [SequenceNumber](#SequenceNumber)
188+
Can _either_ represent an instance of [SequenceNumber](#SequenceNumber)
180189
Or one of:
181190
* `NONE` – No event must match the specified [StreamQuery](#StreamQuery)
182191
* `ANY` – Any event matches (= wildcard [AppendCondition](#AppendCondition))

composer.json

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,7 @@
2020
}
2121
],
2222
"require": {
23-
"php": ">=8.1",
24-
"ramsey/uuid": "^4.7",
23+
"php": ">=8.4",
2524
"psr/clock": "^1",
2625
"webmozart/assert": "^1.11"
2726
},

src/EventStore.php

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
use Wwwision\DCBEventStore\Exceptions\ConditionalAppendFailed;
88
use Wwwision\DCBEventStore\Types\AppendCondition;
9+
use Wwwision\DCBEventStore\Types\Event;
910
use Wwwision\DCBEventStore\Types\Events;
1011
use Wwwision\DCBEventStore\Types\ReadOptions;
1112
use Wwwision\DCBEventStore\Types\StreamQuery\StreamQuery;
@@ -23,21 +24,14 @@ interface EventStore
2324
*/
2425
public function read(StreamQuery $query, ?ReadOptions $options = null): EventStream;
2526

26-
/**
27-
* Returns an event stream that contains all events
28-
*
29-
* @param ReadOptions|null $options optional configuration for this interaction ({@see ReadOptions})
30-
*/
31-
public function readAll(?ReadOptions $options = null): EventStream;
32-
3327
/**
3428
* Commits the specified $events if the specified {@see AppendCondition} is satisfied
3529
*
3630
* NOTE: This is an atomic operation, so either _all_ events will be committed or _none_
3731
*
38-
* @param Events $events The events to append to the event stream
39-
* @param AppendCondition $condition The condition that has to be met
32+
* @param Events|Event $events The events (or a single event) to append to the event stream
33+
* @param AppendCondition $condition The condition that has to be met. Note: use {@see AppendCondition::noConstraints()} to skip constraint checks
4034
* @throws ConditionalAppendFailed If specified $condition is violated
4135
*/
42-
public function append(Events $events, AppendCondition $condition): void;
36+
public function append(Events|Event $events, AppendCondition $condition): void;
4337
}

src/Helpers/InMemoryEventStore.php

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -91,11 +91,6 @@ public function read(StreamQuery $query, ?ReadOptions $options = null): InMemory
9191
return InMemoryEventStream::create(...$matchingEventEnvelopes);
9292
}
9393

94-
public function readAll(?ReadOptions $options = null): EventStream
95-
{
96-
return $this->read(StreamQuery::wildcard(), $options);
97-
}
98-
9994
private static function criterionMatchesEvent(Criterion $criterion, Event $event): bool
10095
{
10196
return match ($criterion::class) {
@@ -104,7 +99,7 @@ private static function criterionMatchesEvent(Criterion $criterion, Event $event
10499
};
105100
}
106101

107-
public function append(Events $events, AppendCondition $condition): void
102+
public function append(Events|Event $events, AppendCondition $condition): void
108103
{
109104
if (!$condition->expectedHighestSequenceNumber->isAny()) {
110105
$lastEventEnvelope = $this->read($condition->query, ReadOptions::create(backwards: true))->first();
@@ -120,6 +115,9 @@ public function append(Events $events, AppendCondition $condition): void
120115
}
121116
$sequenceNumber = SequenceNumber::fromInteger(count($this->eventEnvelopes) + 1);
122117
$newEventEnvelopes = EventEnvelopes::none();
118+
if ($events instanceof Event) {
119+
$events = Events::fromArray([$events]);
120+
}
123121
foreach ($events as $event) {
124122
$newEventEnvelopes = $newEventEnvelopes->append(
125123
new EventEnvelope(

0 commit comments

Comments
 (0)