event_store_client 1.4.9 → 2.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (116) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +30 -145
  3. data/docs/appending_events.md +155 -0
  4. data/docs/catch_up_subscriptions.md +253 -0
  5. data/docs/configuration.md +83 -0
  6. data/docs/deleting_streams.md +25 -0
  7. data/docs/encrypting_events.md +84 -0
  8. data/docs/linking_events.md +149 -0
  9. data/docs/reading_events.md +200 -0
  10. data/lib/event_store_client/adapters/grpc/client.rb +244 -105
  11. data/lib/event_store_client/adapters/grpc/cluster/gossip_discover.rb +131 -0
  12. data/lib/event_store_client/adapters/grpc/cluster/insecure_connection.rb +21 -0
  13. data/lib/event_store_client/adapters/grpc/cluster/member.rb +18 -0
  14. data/lib/event_store_client/adapters/grpc/cluster/queryless_discover.rb +25 -0
  15. data/lib/event_store_client/adapters/grpc/cluster/secure_connection.rb +71 -0
  16. data/lib/event_store_client/adapters/grpc/command_registrar.rb +7 -7
  17. data/lib/event_store_client/adapters/grpc/commands/command.rb +63 -25
  18. data/lib/event_store_client/adapters/grpc/commands/gossip/cluster_info.rb +24 -0
  19. data/lib/event_store_client/adapters/grpc/commands/streams/append.rb +43 -68
  20. data/lib/event_store_client/adapters/grpc/commands/streams/append_multiple.rb +44 -0
  21. data/lib/event_store_client/adapters/grpc/commands/streams/delete.rb +21 -17
  22. data/lib/event_store_client/adapters/grpc/commands/streams/hard_delete.rb +39 -0
  23. data/lib/event_store_client/adapters/grpc/commands/streams/link_to.rb +7 -52
  24. data/lib/event_store_client/adapters/grpc/commands/streams/link_to_multiple.rb +44 -0
  25. data/lib/event_store_client/adapters/grpc/commands/streams/read.rb +20 -85
  26. data/lib/event_store_client/adapters/grpc/commands/streams/read_paginated.rb +174 -0
  27. data/lib/event_store_client/adapters/grpc/commands/streams/subscribe.rb +31 -106
  28. data/lib/event_store_client/adapters/grpc/connection.rb +56 -36
  29. data/lib/event_store_client/adapters/grpc/discover.rb +75 -0
  30. data/lib/event_store_client/adapters/grpc/generated/cluster_pb.rb +106 -18
  31. data/lib/event_store_client/adapters/grpc/generated/cluster_services_pb.rb +12 -12
  32. data/lib/event_store_client/adapters/grpc/generated/code_pb.rb +34 -0
  33. data/lib/event_store_client/adapters/grpc/generated/gossip_pb.rb +3 -2
  34. data/lib/event_store_client/adapters/grpc/generated/gossip_services_pb.rb +3 -3
  35. data/lib/event_store_client/adapters/grpc/generated/monitoring_pb.rb +25 -0
  36. data/lib/event_store_client/adapters/grpc/generated/monitoring_services_pb.rb +26 -0
  37. data/lib/event_store_client/adapters/grpc/generated/operations_pb.rb +2 -1
  38. data/lib/event_store_client/adapters/grpc/generated/operations_services_pb.rb +8 -7
  39. data/lib/event_store_client/adapters/grpc/generated/persistent_pb.rb +199 -38
  40. data/lib/event_store_client/adapters/grpc/generated/persistent_services_pb.rb +7 -3
  41. data/lib/event_store_client/adapters/grpc/generated/projections_pb.rb +9 -26
  42. data/lib/event_store_client/adapters/grpc/generated/projections_services_pb.rb +4 -3
  43. data/lib/event_store_client/adapters/grpc/generated/serverfeatures_pb.rb +29 -0
  44. data/lib/event_store_client/adapters/grpc/generated/serverfeatures_services_pb.rb +26 -0
  45. data/lib/event_store_client/adapters/grpc/generated/shared_pb.rb +54 -12
  46. data/lib/event_store_client/adapters/grpc/generated/status_pb.rb +23 -0
  47. data/lib/event_store_client/adapters/grpc/generated/streams_pb.rb +104 -64
  48. data/lib/event_store_client/adapters/grpc/generated/streams_services_pb.rb +3 -2
  49. data/lib/event_store_client/adapters/grpc/generated/users_services_pb.rb +2 -2
  50. data/lib/event_store_client/adapters/grpc/options/streams/read_options.rb +78 -0
  51. data/lib/event_store_client/adapters/grpc/options/streams/write_options.rb +39 -0
  52. data/lib/event_store_client/adapters/grpc/shared/event_deserializer.rb +52 -0
  53. data/lib/event_store_client/adapters/grpc/shared/options/filter_options.rb +76 -0
  54. data/lib/event_store_client/adapters/grpc/shared/options/stream_options.rb +91 -0
  55. data/lib/event_store_client/adapters/grpc/shared/streams/process_response.rb +28 -0
  56. data/lib/event_store_client/adapters/grpc/shared/streams/process_responses.rb +33 -0
  57. data/lib/event_store_client/adapters/grpc.rb +28 -12
  58. data/lib/event_store_client/configuration.rb +39 -54
  59. data/lib/event_store_client/connection/url.rb +57 -0
  60. data/lib/event_store_client/connection/url_parser.rb +144 -0
  61. data/lib/event_store_client/data_decryptor.rb +2 -9
  62. data/lib/event_store_client/deserialized_event.rb +35 -10
  63. data/lib/event_store_client/encryption_metadata.rb +0 -1
  64. data/lib/event_store_client/event.rb +4 -2
  65. data/lib/event_store_client/extensions/options_extension.rb +87 -0
  66. data/lib/event_store_client/mapper/default.rb +12 -9
  67. data/lib/event_store_client/mapper/encrypted.rb +18 -17
  68. data/lib/event_store_client/types.rb +1 -1
  69. data/lib/event_store_client/utils.rb +30 -0
  70. data/lib/event_store_client/version.rb +1 -1
  71. data/lib/event_store_client.rb +8 -7
  72. metadata +75 -84
  73. data/lib/event_store_client/adapters/grpc/Protos/cluster.proto +0 -149
  74. data/lib/event_store_client/adapters/grpc/Protos/gossip.proto +0 -44
  75. data/lib/event_store_client/adapters/grpc/Protos/operations.proto +0 -45
  76. data/lib/event_store_client/adapters/grpc/Protos/persistent.proto +0 -180
  77. data/lib/event_store_client/adapters/grpc/Protos/projections.proto +0 -174
  78. data/lib/event_store_client/adapters/grpc/Protos/shared.proto +0 -22
  79. data/lib/event_store_client/adapters/grpc/Protos/streams.proto +0 -242
  80. data/lib/event_store_client/adapters/grpc/Protos/users.proto +0 -119
  81. data/lib/event_store_client/adapters/grpc/README.md +0 -16
  82. data/lib/event_store_client/adapters/grpc/commands/persistent_subscriptions/create.rb +0 -46
  83. data/lib/event_store_client/adapters/grpc/commands/persistent_subscriptions/delete.rb +0 -34
  84. data/lib/event_store_client/adapters/grpc/commands/persistent_subscriptions/read.rb +0 -77
  85. data/lib/event_store_client/adapters/grpc/commands/persistent_subscriptions/settings_schema.rb +0 -38
  86. data/lib/event_store_client/adapters/grpc/commands/persistent_subscriptions/update.rb +0 -48
  87. data/lib/event_store_client/adapters/grpc/commands/projections/create.rb +0 -48
  88. data/lib/event_store_client/adapters/grpc/commands/projections/delete.rb +0 -34
  89. data/lib/event_store_client/adapters/grpc/commands/projections/update.rb +0 -44
  90. data/lib/event_store_client/adapters/grpc/commands/streams/read_all.rb +0 -43
  91. data/lib/event_store_client/adapters/grpc/commands/streams/tombstone.rb +0 -35
  92. data/lib/event_store_client/adapters/http/README.md +0 -16
  93. data/lib/event_store_client/adapters/http/client.rb +0 -161
  94. data/lib/event_store_client/adapters/http/commands/command.rb +0 -27
  95. data/lib/event_store_client/adapters/http/commands/persistent_subscriptions/ack.rb +0 -15
  96. data/lib/event_store_client/adapters/http/commands/persistent_subscriptions/create.rb +0 -35
  97. data/lib/event_store_client/adapters/http/commands/persistent_subscriptions/read.rb +0 -60
  98. data/lib/event_store_client/adapters/http/commands/projections/create.rb +0 -33
  99. data/lib/event_store_client/adapters/http/commands/projections/update.rb +0 -31
  100. data/lib/event_store_client/adapters/http/commands/streams/append.rb +0 -49
  101. data/lib/event_store_client/adapters/http/commands/streams/delete.rb +0 -16
  102. data/lib/event_store_client/adapters/http/commands/streams/link_to.rb +0 -49
  103. data/lib/event_store_client/adapters/http/commands/streams/read.rb +0 -52
  104. data/lib/event_store_client/adapters/http/commands/streams/tombstone.rb +0 -17
  105. data/lib/event_store_client/adapters/http/connection.rb +0 -46
  106. data/lib/event_store_client/adapters/http/request_method.rb +0 -28
  107. data/lib/event_store_client/adapters/http.rb +0 -17
  108. data/lib/event_store_client/adapters/in_memory.rb +0 -144
  109. data/lib/event_store_client/broker.rb +0 -40
  110. data/lib/event_store_client/catch_up_subscription.rb +0 -42
  111. data/lib/event_store_client/catch_up_subscriptions.rb +0 -92
  112. data/lib/event_store_client/client.rb +0 -73
  113. data/lib/event_store_client/error_handler.rb +0 -10
  114. data/lib/event_store_client/subscription.rb +0 -23
  115. data/lib/event_store_client/subscriptions.rb +0 -38
  116. data/lib/event_store_client/value_objects/read_direction.rb +0 -43
@@ -0,0 +1,83 @@
1
+ # Configuration
2
+
3
+ Currently only one setup is supported. For example, you can't configure a connection to multiple clusters.
4
+
5
+ Configuration options:
6
+
7
+ | name | value | default value | description |
8
+ |----------------------|--------------------------------------------------------------------------------------|-----------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
9
+ | event_store_url | String | `'esdb://localhost:2113'` | Connection string. See description of possible values below. |
10
+ | per_page | Integer | `20` | Number of events to return in one response. |
11
+ | mapper | `EventStoreClient::Mapper::Default.new` or `EventStoreClient::Mapper::Encrypted.new` | `EventStoreClient::Mapper::Default.new` | An object that is responsible for serialization / deserialization and encryption / decryption of events. |
12
+ | default_event_class | `DeserializedEvent` or any class, inherited from it | `DeserializedEvent` | This class will be used during the deserialization process when deserializer fails to resolve an event's class from response. |
13
+ | logger | `Logger` | `nil` | A logger that would log messages from `event_store_client` and `grpc` gems. |
14
+ | skip_deserialization | Boolean | `false` | Whether to skip event deserialization using the given `mapper` setting. If you set it to `true` decryption will be skipped as well. It is useful when you want to defer deserialization and handle it later by yourself. |
15
+ | skip_decryption | Boolean | `false` | Whether to skip decrypting encrypted event payloads. |
16
+
17
+ ## Connection string
18
+
19
+ Connection string allows you to provide connection options and a set of nodes of your cluster to connect to.
20
+
21
+ Structure:
22
+
23
+ ```
24
+ protocol://[username:password@]node1[,node2,node3,...,nodeN]/?connectionOptions
25
+ ```
26
+
27
+ ### Protocol
28
+
29
+ There are two possible values:
30
+
31
+ - `esdb`. Currently no effect.
32
+ - `esdb+discover`. `+discover` tells the client that your cluster is setup as [DNS discovery](https://developers.eventstore.com/server/v20.10/cluster.html#cluster-with-dns). This means that the client will perform a lookup of cluster members of the first node you provided in the connection string.
33
+
34
+ Examples:
35
+ ```
36
+ esdb://localhost:2113
37
+ esdb+discover://localhost:2113
38
+ ```
39
+
40
+ ### Credentials
41
+
42
+ You may provide a username and password to be used to connect to the EventStore DB. Only secure connections will use those credentials. Only credentials defined with first node will be used.
43
+
44
+ Examples:
45
+
46
+ ```
47
+ esdb://some-admin:some-password@localhost:2113
48
+ esdb://some-admin:some-password@localhost:2113,localhost:2114
49
+ ```
50
+
51
+ ### Nodes
52
+
53
+ At least one node should be defined in the connection string in order for the client to work properly. You may define as much nodes as you want. However, the behavior of the client may change depending on how many nodes you provided and whether you set the `+discover` flag.
54
+
55
+ Possible behaviours:
56
+
57
+ - One node is provided. E.g. `esdb://localhost:2113`. The client assumes this setup is a standalone server - no cluster discovery will be done.
58
+ - One node and `+discover` flag is provided. E.g. `esdb+discover://localhost:2113`. The client assumes this setup is a cluster with DNS discover setup - cluster discovery will be done.
59
+ - Two or more nodes are provided. E.g. `esdb://localhost:2113,localhost:2114,localhost:2115`. The client assumes this setup is a nodes cluster setup - cluster discovery will be done. If discovery of a node fails, the next node in the list will be picked, and so forth, in round-robin manner, until max discovery attempts number is reached.
60
+
61
+ If you provide more than one node and set `+discover` flag - only the first node will be considered.
62
+
63
+ ### Connection options
64
+
65
+ Connection options allows you to adjust parameters such as timeout, security, etc.
66
+
67
+ Possible options:
68
+
69
+ | name | value | default value | description |
70
+ |----------------------|-------------------------------------------------|---------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
71
+ | tls | Boolean | `true` | Whether to use a secure connection |
72
+ | tlsVerifyCert | Boolean | `false` | Whether to verify a certificate |
73
+ | tlsCAFile | String | `nil` | A path to a certificate file (e.g. `ca.crt`). If you set the `tls` option to `true`, but didn't provide this option, the client will try to automatically retrieve an X.509 certificate from the nodes. |
74
+ | gossipTimeout | Integer | `200` | Milliseconds. Discovery request timeout. Only useful when there are several nodes or when `+discover` flag is set. |
75
+ | discoverInterval | Integer | `100` | Milliseconds. Interval between discovery attempts. Only useful when there are several nodes or when `+discover` flag is set. |
76
+ | maxDiscoverAttempts | Integer | `10` | Max attempts before giving up to find a suitable cluster member. Only useful when there are several nodes or when the `+discover` flag is set. |
77
+ | caLookupInterval | Integer | `100` | Milliseconds. Interval between X.509 certificate lookup attempts. This option is useful when you set the`tls` option to true, but you didn't provide the `tlsCAFile` option. In this case the certificate will be retrieved using the Net::HTTP#peer_cert method. |
78
+ | caLookupAttempts | Integer | `3` | Number of attempts for X.509 certificate lookup. This option is useful when you set the `tls` option to true, but you didn't provide the `tlsCAFile` option. In this case the certificate will be retrieved using Net::HTTP#peer_cert method. |
79
+ | nodePreference | `"leader"`, `"follower"` or `"readOnlyReplica"` | `"leader"` | Set which state of cluster members is preferred. Only useful if you provided the `+discover` flag or defined several nodes in the connection string. |
80
+ | timeout | Integer, `nil` | `nil` | Milliseconds. Defines how long to wait for a response before throwing an error. This option doesn't apply to subscriptions. If set to `nil` - `event_store_client` will be waiting for a response forever (if there is a connection at all). |
81
+ | grpcRetryAttempts | Integer | `3` | Number of times to retry GRPC requests. Does not apply to discovery requests. Final number of requests in cases of error will be initial request + grpcRetryAttempts. |
82
+ | grpcRetryInterval | Integer | `100` | Milliseconds. Delay between GRPC requests. retries. |
83
+ | throwOnAppendFailure | Boolean | `true` | Defines if append requests should immediately raise an error. If set to `false`, request will be retried in case of a server error. |
@@ -0,0 +1,25 @@
1
+ # Deleting streams
2
+
3
+ ## Soft deleting streams
4
+
5
+ When you read a soft deleted stream, the read returns `:stream_not_found` or 404 result. After deleting the stream, you are able to append to it again, continuing from where it left off.
6
+
7
+ ```ruby
8
+ EventStoreClient.client.delete_stream('some-stream')
9
+ ```
10
+
11
+ ## Hard deleting streams
12
+
13
+ A hard delete of a stream is permanent. You cannot append to the stream or recreate it. As such, you should generally soft delete streams unless you have a specific need to permanently delete the stream.
14
+
15
+ ```ruby
16
+ EventStoreClient.client.hard_delete_stream('some-stream')
17
+ ```
18
+
19
+ ## User credentials
20
+
21
+ You can provide user credentials to be used to delete the stream as follows. This will override the default credentials set on the connection.
22
+
23
+ ```ruby
24
+ EventStoreClient.client.delete_stream('some-stream', credentials: { username: 'admin', password: 'changeit' })
25
+ ```
@@ -0,0 +1,84 @@
1
+ # Encrypting events
2
+
3
+ To encrypt/decrypt events payload, you can use an encrypted mapper.
4
+
5
+
6
+ ```ruby
7
+ mapper = EventStoreClient::Mapper::Encrypted.new(key_repository)
8
+
9
+ EventStoreClient.configure do |config|
10
+ config.mapper = mapper
11
+ end
12
+ ```
13
+
14
+ The Encrypted mapper uses the encryption key repository to encrypt data in your events according to the event definition.
15
+
16
+ Here is the minimal repository interface for this to work.
17
+
18
+ ```ruby
19
+ class DummyRepository
20
+ class Key
21
+ attr_accessor :iv, :cipher, :id
22
+ def initialize(id:, **)
23
+ @id = id
24
+ end
25
+ end
26
+
27
+ def find(user_id)
28
+ Key.new(id: user_id)
29
+ end
30
+
31
+ def encrypt(*)
32
+ 'darthvader'
33
+ end
34
+
35
+ def decrypt(*)
36
+ { first_name: 'Anakin', last_name: 'Skywalker'}
37
+ end
38
+ end
39
+ ```
40
+
41
+ Now, having that, you only need to define the event encryption schema:
42
+
43
+ ```ruby
44
+ class EncryptedEvent < EventStoreClient::DeserializedEvent
45
+ def schema
46
+ Dry::Schema.Params do
47
+ required(:user_id).value(:string)
48
+ required(:first_name).value(:string)
49
+ required(:last_name).value(:string)
50
+ required(:profession).value(:string)
51
+ end
52
+ end
53
+
54
+ def self.encryption_schema
55
+ {
56
+ key: ->(data) { data['user_id'] },
57
+ attributes: %i[first_name last_name email]
58
+ }
59
+ end
60
+ end
61
+
62
+ event = EncryptedEvent.new(
63
+ user_id: SecureRandom.uuid,
64
+ first_name: 'Anakin',
65
+ last_name: 'Skywalker',
66
+ profession: 'Jedi'
67
+ )
68
+ ```
69
+
70
+ When you publish this event, the eventstore will store this payload:
71
+
72
+ ```ruby
73
+ {
74
+ 'data' => {
75
+ 'user_id' => 'dab48d26-e4f8-41fc-a9a8-59657e590716',
76
+ 'first_name' => 'encrypted',
77
+ 'last_name' => 'encrypted',
78
+ 'profession' => 'Jedi',
79
+ 'encrypted' => '2345l423lj1#$!lkj24f1'
80
+ },
81
+ type: 'EncryptedEvent'
82
+ metadata: { ... }
83
+ }
84
+ ```
@@ -0,0 +1,149 @@
1
+ # Linking events
2
+
3
+ ## Linking single event
4
+
5
+ To create a link on an existing event, you need to have a stream name where you want to link that event to and you need to have an event, fetched from the database:
6
+
7
+ ```ruby
8
+ class SomethingHappened < EventStoreClient::DeserializedEvent
9
+ end
10
+
11
+ event = SomethingHappened.new(
12
+ id: SecureRandom.uuid, type: 'some-event', data: { user_id: SecureRandom.uuid, title: "Something happened" }
13
+ )
14
+
15
+ stream_name_1 = 'some-stream-1'
16
+ stream_name_2 = 'some-stream-2'
17
+ EventStoreClient.client.append_to_stream(stream_name_1, event)
18
+ # Get persisted event
19
+ event = EventStoreClient.client.read(stream_name_1).success.first
20
+ # Link event from first stream into second stream
21
+ result = EventStoreClient.client.link_to(stream_name_2, event)
22
+ if result.success? # Event was successfully linked
23
+ else # event was not linked, result.failure? => true
24
+ end
25
+ ```
26
+
27
+ The linked event can later be fetched by providing the `:resolve_link_tos` option when reading from the stream:
28
+
29
+ ```ruby
30
+ EventStoreClient.client.read('some-stream-2', options: { resolve_link_tos: true }).success
31
+ ```
32
+
33
+ If you don't provide the `:resolve_link_tos` option, the "linked" event will be returned instead of the original one.
34
+
35
+ ## Linking multiple events
36
+
37
+ You can provide an array of events to link to the target stream:
38
+
39
+ ```ruby
40
+ class SomethingHappened < EventStoreClient::DeserializedEvent
41
+ end
42
+
43
+ events =
44
+ 3.times.map do
45
+ SomethingHappened.new(
46
+ id: SecureRandom.uuid, type: 'some-event', data: { user_id: SecureRandom.uuid, title: "Something happened" }
47
+ )
48
+ end
49
+
50
+ stream_name_1 = 'some-stream-1'
51
+ stream_name_2 = 'some-stream-2'
52
+ events.each do |event|
53
+ EventStoreClient.client.append_to_stream(stream_name_1, event)
54
+ end
55
+ # Get persisted events
56
+ events = EventStoreClient.client.read(stream_name_1).success
57
+ # Link events from first stream into second stream one by one
58
+ results = EventStoreClient.client.link_to(stream_name_2, events)
59
+ results.each do |result|
60
+ if result.success? # Event was successfully linked
61
+ else # event was not linked, result.failure? => true
62
+ end
63
+ end
64
+ ```
65
+
66
+ ## Handling concurrency
67
+
68
+ When linking events to a stream you can supply a stream state or stream revision. Your client can use this to tell EventStoreDB what state or version you expect the stream to be in when you append. If the stream isn't in that state then an exception will be raised.
69
+
70
+ For example if we try and link two records expecting both times that the stream doesn't exist we will get an exception on the second:
71
+
72
+ ```ruby
73
+ class SomethingHappened < EventStoreClient::DeserializedEvent
74
+ end
75
+
76
+ event1 = SomethingHappened.new(
77
+ type: 'some-event', data: {},
78
+ )
79
+
80
+ event2 = SomethingHappened.new(
81
+ type: 'some-event', data: {},
82
+ )
83
+
84
+ stream_name_1 = "some-stream-1$#{SecureRandom.uuid}"
85
+ stream_name_2 = "some-stream-2$#{SecureRandom.uuid}"
86
+
87
+ EventStoreClient.client.append_to_stream(stream_name_1, [event1, event2])
88
+ events = EventStoreClient.client.read(stream_name_1).success
89
+
90
+ results = EventStoreClient.client.link_to(stream_name_2, events, options: { expected_revision: :no_stream })
91
+ results[0].success? # => true
92
+ results[1].success? # => false because second request tries to link the event with `:no_stream` expected revision
93
+ ```
94
+
95
+ There are three available stream states:
96
+
97
+ - `:any`
98
+ - `:no_stream`
99
+ - `:stream_exists`
100
+
101
+ This check can be used to implement optimistic concurrency. When you retrieve a stream from EventStoreDB, you take note of the current version number, then when you save it back you can determine if somebody else has modified the record in the meantime.
102
+
103
+ ```ruby
104
+ class SomethingHappened < EventStoreClient::DeserializedEvent
105
+ end
106
+
107
+ stream_name_1 = "some-stream-1$#{SecureRandom.uuid}"
108
+ stream_name_2 = "some-stream-2$#{SecureRandom.uuid}"
109
+ event1 = SomethingHappened.new(
110
+ type: 'some-event', data: {}
111
+ )
112
+ event2 = SomethingHappened.new(
113
+ type: 'some-event', data: {}
114
+ )
115
+
116
+ # Pre-create some events
117
+ EventStoreClient.client.append_to_stream(stream_name_1, event1)
118
+ EventStoreClient.client.append_to_stream(stream_name_2, event2)
119
+ # Load events from DB
120
+ event1 = EventStoreClient.client.read(stream_name_1).success.first
121
+ event2 = EventStoreClient.client.read(stream_name_2).success.first
122
+ # Get the revision number of latest event
123
+ revision = EventStoreClient.client.read(stream_name_2).success.last.stream_revision
124
+ # Expected revision matches => will succeed
125
+ EventStoreClient.client.link_to(stream_name_2, event1, options: { expected_revision: revision })
126
+ # Will fail with revisions mismatch error
127
+ EventStoreClient.client.link_to(stream_name_2, event2, options: { expected_revision: revision })
128
+ ```
129
+
130
+ ## User credentials
131
+
132
+ You can provide user credentials to be used to append the data as follows. This will override the default credentials set on the connection.
133
+
134
+ ```ruby
135
+ class SomethingHappened < EventStoreClient::DeserializedEvent
136
+ end
137
+
138
+ event = SomethingHappened.new(
139
+ id: SecureRandom.uuid, type: 'some-event', data: { user_id: SecureRandom.uuid, title: "Something happened" }
140
+ )
141
+
142
+ stream_name_1 = 'some-stream-1'
143
+ stream_name_2 = 'some-stream-2'
144
+ EventStoreClient.client.append_to_stream(stream_name_1, event)
145
+ # Get persisted event
146
+ event = EventStoreClient.client.read(stream_name_1).success.first
147
+ # Link event from first stream into second stream
148
+ result = EventStoreClient.client.link_to(stream_name_2, event, credentials: { username: 'admin', password: 'changeit' })
149
+ ```
@@ -0,0 +1,200 @@
1
+ # Reading events
2
+
3
+ ## Reading from a stream
4
+
5
+ The simplest way to read a stream forwards is to supply a stream name.
6
+
7
+ ```ruby
8
+ EventStoreClient.client.read('some-stream')
9
+ # => Success([#<EventStoreClient::DeserializedEvent 0x1>, #<EventStoreClient::DeserializedEvent 0x1>])
10
+ ```
11
+
12
+ This will return either `Dry::Monads::Success` with the list of events attached or `Dry::Monads::Failure` with an error. You can handle the result like this:
13
+
14
+ ```ruby
15
+ result = EventStoreClient.client.read('some-stream')
16
+ if result.success?
17
+ result.success.each do |event|
18
+ # do something with an event
19
+ end
20
+ end
21
+ ```
22
+
23
+ ### Request options customization
24
+
25
+ You can provide a block to the `#read` method. Request options will be yielded right before the request, allowing you to set advanced options:
26
+
27
+ ```ruby
28
+ EventStoreClient.client.read('some-stream') do |opts|
29
+ opts.control_option = EventStore::Client::Streams::ReadReq::Options::ControlOption.new(
30
+ compatibility: 1
31
+ )
32
+ end
33
+ ```
34
+
35
+ ### max_count
36
+
37
+ You can provide the `:max_count` option. This option determines how much records to return in a response:
38
+
39
+ ```ruby
40
+ EventStoreClient.client.read('some-stream', options: { max_count: 1000 })
41
+ ```
42
+
43
+ ### resolve_link_tos
44
+
45
+ When using projections to create new events you can set whether the generated events are pointers to existing events. Setting this value to `true` tells EventStoreDB to return the event as well as the event linking to it.
46
+
47
+ ```ruby
48
+ EventStoreClient.client.read('some-stream', options: { resolve_link_tos: true })
49
+ ```
50
+
51
+ ### from_revision
52
+
53
+ You can define from which revision number you would like to start to read events:
54
+
55
+ ```ruby
56
+ EventStoreClient.client.read('some-stream', options: { from_revision: 2 })
57
+ ```
58
+
59
+ Acceptable values are: number, `:start` and `:end`
60
+
61
+ ### direction
62
+
63
+ As well as being able to read a stream forwards you can also go backwards. This can be achieved by providing the `:direction` option:
64
+
65
+ ```ruby
66
+ EventStoreClient.client.read('some-stream', options: { direction: 'Backwards', from_revision: :end })
67
+ ```
68
+
69
+ ## Checking if the stream exists
70
+
71
+ In case a stream with given name does not exist, `Dry::Monads::Failure` will be returned with value `:stream_not_found`:
72
+
73
+ ```ruby
74
+ result = EventStoreClient.client.read('non-existing-stream')
75
+ # => Failure(:stream_not_found)
76
+ result.failure?
77
+ # => true
78
+ result.failure
79
+ # => :stream_not_found
80
+ ```
81
+
82
+ ## Reading from the $all stream
83
+
84
+ Simply supply `"$all"` as the stream name in `#read`:
85
+
86
+ ```ruby
87
+ EventStoreClient.client.read('$all')
88
+ ```
89
+
90
+ The only difference in reading from `$all` vs reading from specific stream is that you should provide `:from_position` option instead `:from_revision` in order to define a position from which to read:
91
+
92
+ ```ruby
93
+ EventStoreClient.client.read('$all', options: { from_position: :start })
94
+ EventStoreClient.client.read('$all', options: { from_position: :end, direction: 'Backwards' })
95
+ EventStoreClient.client.read('$all', options: { from_position: { commit_position: 9023, prepare_position: 9023 } })
96
+ ```
97
+
98
+ ## Result deserialization
99
+
100
+ If you would like to skip deserialization of the `#read` result, you should use the `:skip_deserialization` argument. This way you will receive the result from EventStore DB as is, including system events, etc:
101
+
102
+ ```ruby
103
+ EventStoreClient.client.read('some-stream', skip_deserialization: true)
104
+ # => Success([<EventStore::Client::Streams::ReadResp ...>])
105
+ ```
106
+
107
+ ## Filtering
108
+
109
+ The filtering feature is only available for the`$all` stream.
110
+
111
+ Retrieve events from streams with name starting with `some-stream`:
112
+
113
+ ```ruby
114
+ result =
115
+ EventStoreClient.client.read('$all', options: { filter: { stream_identifier: { prefix: ['some-stream'] } } })
116
+ if result.success?
117
+ result.success.each do |e|
118
+ # iterate through events
119
+ end
120
+ end
121
+ ```
122
+
123
+ Retrieve events with name starting with `some-event`:
124
+
125
+ ```ruby
126
+ result =
127
+ EventStoreClient.client.read('$all', options: { event_type: { prefix: ['some-event'] } })
128
+ if result.success?
129
+ result.success.each do |e|
130
+ # iterate through events
131
+ end
132
+ end
133
+ ```
134
+
135
+ Retrieving events from stream `some-stream-1` and `some-stream-2`:
136
+
137
+ ```ruby
138
+ result =
139
+ EventStoreClient.client.read('$all', options: { filter: { stream_identifier: { prefix: ['some-stream-1', 'some-stream-2'] } } })
140
+ if result.success?
141
+ result.success.each do |e|
142
+ # iterate through events
143
+ end
144
+ end
145
+ ```
146
+
147
+ ## Pagination
148
+
149
+ You can use `#read_paginated`, the ready-to-go implementation of pagination which returns an array of result pages:
150
+
151
+ ```ruby
152
+ EventStoreClient.client.read_paginated('some-stream').each do |result|
153
+ if result.success?
154
+ result.success.each do |event|
155
+ # do something with event
156
+ end
157
+ end
158
+ end
159
+
160
+ EventStoreClient.client.read_paginated('$all').each do |result|
161
+ if result.success?
162
+ result.success.each do |event|
163
+ # do something with event
164
+ end
165
+ end
166
+ end
167
+ ```
168
+
169
+
170
+
171
+ ### Paginating backward reads
172
+
173
+ Just supply a call with `:direction` option and with `:from_position`/`:from_revision` option(depending on what stream you read from):
174
+
175
+ ```ruby
176
+ EventStoreClient.client.read_paginated('some-stream', options: { direction: 'Backwards', from_revision: :end }).each do |result|
177
+ if result.success?
178
+ result.each do |event|
179
+ # do something with event
180
+ end
181
+ end
182
+ end
183
+
184
+ EventStoreClient.client.read_paginated('$all', options: { direction: 'Backwards', from_position: :end }).each do |result|
185
+ if result.success?
186
+ result.each do |event|
187
+ # do something with event
188
+ end
189
+ end
190
+ end
191
+ ```
192
+
193
+ Note: for some reason when paginating the `$all` stream, EventStoreDB returns duplicate records from page to page (first event of `page N` = last event of `page N - 1`).
194
+ ## User credentials
195
+
196
+ You can provide user credentials to be used to read the data as follows. This will override the default credentials set on the connection.
197
+
198
+ ```ruby
199
+ EventStoreClient.client.read('some-stream', credentials: { username: 'admin', password: 'changeit' })
200
+ ```