Skip to content

Commit deaf5ff

Browse files
feat: Add support for the isEventQlTrue precondition (#108)
* feat: add IsEventQLTrue precondition and corresponding tests * chore: adjust precondition casing * chore: adjust readme * chore: remove escape sequences from example query in README
1 parent 2d642df commit deaf5ff

File tree

7 files changed

+93
-6
lines changed

7 files changed

+93
-6
lines changed

README.md

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,25 @@ written_events = await client.write_events(
105105

106106
*Note that according to the CloudEvents standard, event IDs must be of type string.*
107107

108+
#### Using the `isEventQlTrue` precondition
109+
110+
If you want to write events depending on an EventQL query, import the `IsEventQlTrue` class and pass it as a list of preconditions in the second argument:
111+
112+
```python
113+
from eventsourcingdb import IsEventQlTrue
114+
115+
written_events = await client.write_events(
116+
events = [
117+
# events
118+
],
119+
preconditions = [
120+
IsEventQlTrue('FROM e IN events WHERE e.type == "io.eventsourcingdb.library.book-borrowed" PROJECT INTO COUNT() < 10')
121+
],
122+
)
123+
```
124+
125+
*Note that the query must return a single row with a single value, which is interpreted as a boolean.*
126+
108127
### Reading Events
109128

110129
To read all events of a subject, call the `read_events` function with the subject as the first argument and an options object as the second argument. Set the `recursive` option to `False`. This ensures that only events of the given subject are returned, not events of nested subjects.
@@ -530,4 +549,4 @@ In case you need to set up the client yourself, use the following functions to g
530549
- `get_host()` returns the host name
531550
- `get_mapped_port()` returns the port
532551
- `get_base_url()` returns the full URL of the container
533-
- `get_api_token()` returns the API token
552+
- `get_api_token()` returns the API token

eventsourcingdb/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
)
1111
from .read_event_types import EventType
1212
from .read_events import IfEventIsMissingDuringRead, Order, ReadEventsOptions, ReadFromLatestEvent
13-
from .write_events import IsSubjectOnEventId, IsSubjectPristine, Precondition
13+
from .write_events import IsEventQlTrue, IsSubjectOnEventId, IsSubjectPristine, Precondition
1414

1515
__all__ = [
1616
"Bound",
@@ -25,6 +25,7 @@
2525
"IfEventIsMissingDuringObserve",
2626
"IfEventIsMissingDuringRead",
2727
"InternalError",
28+
"IsEventQlTrue",
2829
"IsSubjectOnEventId",
2930
"IsSubjectPristine",
3031
"ObserveEventsOptions",

eventsourcingdb/write_events/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
from .preconditions import IsSubjectOnEventId, IsSubjectPristine, Precondition
1+
from .preconditions import IsEventQlTrue, IsSubjectOnEventId, IsSubjectPristine, Precondition
22

33
__all__ = [
4+
"IsEventQlTrue",
45
"IsSubjectOnEventId",
56
"IsSubjectPristine",
67
"Precondition",

eventsourcingdb/write_events/preconditions.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,15 @@ def to_json(self) -> dict[str, Any]:
2727
"type": "isSubjectOnEventId",
2828
"payload": {"subject": self.subject, "eventId": self.event_id},
2929
}
30+
31+
@dataclass
32+
class IsEventQlTrue(Precondition):
33+
query: str
34+
35+
def to_json(self) -> dict[str, Any]:
36+
return {
37+
'type': 'isEventQlTrue',
38+
'payload': {
39+
'query': self.query
40+
}
41+
}
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
FROM thenativeweb/eventsourcingdb:1.0.3
1+
FROM thenativeweb/eventsourcingdb:preview

tests/test_write_events.py

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import pytest
22
from aiohttp import ClientConnectorDNSError
33

4-
from eventsourcingdb import EventCandidate, IsSubjectOnEventId, IsSubjectPristine, ServerError
4+
from eventsourcingdb import EventCandidate, IsEventQlTrue, IsSubjectOnEventId, IsSubjectPristine, ServerError
55

66
from .conftest import TestData
77
from .shared.database import Database
@@ -184,6 +184,60 @@ async def test_is_subject_on_event_id_fails_for_wrong_id(
184184
[IsSubjectOnEventId("/", "2")],
185185
)
186186

187+
@staticmethod
188+
@pytest.mark.asyncio
189+
async def test_is_event_ql_true_precondition_works(
190+
database: Database,
191+
test_data: TestData,
192+
) -> None:
193+
client = database.get_client()
194+
195+
await client.write_events(
196+
[
197+
EventCandidate(
198+
source=test_data.TEST_SOURCE_STRING, subject="/", type="com.foo.bar", data={}
199+
)
200+
]
201+
)
202+
203+
await client.write_events(
204+
[
205+
EventCandidate(
206+
source=test_data.TEST_SOURCE_STRING, subject="/", type="com.foo.bar", data={}
207+
)
208+
],
209+
[IsEventQlTrue("FROM e IN events PROJECT INTO COUNT() > 0")]
210+
)
211+
212+
@staticmethod
213+
@pytest.mark.asyncio
214+
async def test_is_event_ql_true_precondition_fails(
215+
database: Database,
216+
test_data: TestData,
217+
) -> None:
218+
client = database.get_client()
219+
220+
await client.write_events(
221+
[
222+
EventCandidate(
223+
source=test_data.TEST_SOURCE_STRING, subject="/", type="com.foo.bar", data={}
224+
)
225+
]
226+
)
227+
228+
with pytest.raises(ServerError):
229+
await client.write_events(
230+
[
231+
EventCandidate(
232+
source=test_data.TEST_SOURCE_STRING,
233+
subject="/",
234+
type="com.foo.bar",
235+
data={},
236+
)
237+
],
238+
[IsEventQlTrue("FROM e IN events PROJECT INTO COUNT() == 0")]
239+
)
240+
187241
@staticmethod
188242
@pytest.mark.asyncio
189243
async def test_throws_error_if_event_does_not_match_schema(

uv.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)