View Source Spear.Event (Spear v1.4.1)
A simplified event struct
This event struct is easier to work with than the protobuf definitions for AppendReq and ReadResp records
Summary
Functions
Converts a read-response message to a Spear.Event
Returns the ID of the event, following the event's link if provided
Creates an event struct
Returns the revision of the event, following the event's link if provided
Converts an event into a checkpoint
Converts a Spear.Event
into an append-request record which proposes a new
message
Produces a random UUID v4 in human-readable format
Produces a consistent UUID v4 in human-readable format given any input data structure
Types
@type t() :: %Spear.Event{ body: term(), id: String.t(), link: t() | nil, metadata: map(), type: String.t() }
A struct representing an EventStoreDB event
Spear.Event.t/0
s may be created to write to the EventStoreDB with
new/3
. Spear.Event.t/0
s will be lazily mapped into
gRPC-compatible structs before being written to the EventStoreDB with
to_proposed_message/2
.
Spear.Event.t/0
s typically look different between events which are
written to- and events which are read from the EventStoreDB. Read events
contain more metadata which pertains to EventStoreDB specifics like
the creation timestamp of the event.
Links
The :link
field of a Spear.Event.t/0
can contain another
Spear.Event.t/0
struct. Link events are pointers to other events and are
used by the EventStoreDB to provide the projections feature without having
to duplicate events across streams. Links do not usually contain any
information useful to consumers, but clients must keep track of links in
order to keep an accurate position in a projected stream, or in order to
Spear.ack/3
/Spear.nack/4
the projected events in a persistent
subscription.
The :from
option in functions like Spear.read_stream/3
, Spear.stream!/3
or Spear.subscribe/4
will use the link event's .metadata.stream_revision
when reading projected streams. Spear.ack/3
and Spear.nack/4
take
the link's :id
field when passed a Spear.Event.t/0
. When not passing
Spear.Event.t/0
s (for example, if curating the stream revisions or IDs
in a database), the revision/1
and id/1
functions may be used to return
the proper metadata for standard subscriptions and persistent subscriptions,
respectively.
Examples
iex> Spear.stream!(conn, "es_supported_clients") |> Enum.take(1)
[
%Spear.Event{
body: %{"languages" => ["typescript", "javascript"], "runtime" => "NodeJS"},
id: "1fc908c1-af32-4d06-a9bd-3bf86a833fdf",
metadata: %{
commit_position: 18446744073709551615,
content_type: "application/json",
created: ~U[2021-04-01 21:11:38.196799Z],
custom_metadata: "",
prepare_position: 18446744073709551615,
stream_name: "es_supported_clients",
stream_revision: 0
},
type: "grpc-client",
link: nil
}
]
iex> Spear.Event.new("grpc-client", %{"languages" => ["typescript", "javascript"], "runtime" => "NodeJS"},
%Spear.Event{
body: %{"languages" => ["typescript", "javascript"], "runtime" => "NodeJS"},
id: "b952575a-1014-404d-ba20-f0904df7954e",
metadata: %{content_type: "application/json", custom_metadata: ""},
type: "grpc-client",
link: nil
}
Functions
Converts a read-response message to a Spear.Event
This function is applied by Stream.map/2
onto streams returned by
reading operations such as Spear.stream!/3
, Spear.read_stream/3
, etc.
by default. This can be turned off by passing the raw?: true
opt to
a reading function.
This function follows links. For example, if an read event belongs to a
projected stream such as an event type stream, this function will give the
event body of the source event, not the link. Forcing the return of the
link body can be accomplished with the :link?
option set to true
(it is
false
by default).
Options
:link?
- (default:false
) forces returning the body of the link event for events read from projected streams. Has no effect on events from non- projected streams.:json_decoder
- (default:Jason.decode!/2
) a 2-arity function to use for events with a"content-type"
of"application/json"
.
All remaining options passed as opts
other than :link?
and
:json_decoder
are passed to the second argument of the :json_decoder
2-arity function.
JSON decoding
Event bodies are commonly written to the EventStoreDB in JSON format as the
format is a human-readable and supported in nearly any language. Events
carry a small piece of metadata in the ReadResp.ReadEvent.RecordedEvent
's
:metadata
map field which declares the content-type of the event body:
"content-type"
. This function will automatically attempt to decode any
events which declare an "application/json"
content-type as JSON using
the :json_decoder
2-arity function option. Other content-types will not
trigger any automatic behavior.
Spear
takes an optional dependency on the Jason
library as it is
currently the most popular JSON (en/de)coding library. If you add this
project to the deps/0
in a mix.exs
file and wish to take advantage of
the automatic JSON decoding functionality, you may also need to include
:jason
. As an optional dependency, :jason
is not included in your
dependencies just by dependending on :spear
.
# mix.exs
def deps do
[
{:spear, ">= 0.0.0"},
{:jason, ">= 0.0.0"},
..
]
end
Other JSON (en/de)coding libraries may be swapped in, such as with Poison
iex> Spear.stream!(conn, "es_supported_clients", raw?: true)
...> |> Stream.map(&Spear.Event.from_read_response(&1, json_decoder: &Poison.decode!/2, keys: :atoms))
Examples
Spear.stream!(conn, "es_supported_clients", raw?: true)
|> Stream.map(&Spear.Event.from_read_response/1)
|> Enum.to_list()
# => [%Spear.Event{}, %Spear.Event{}, ..]
Returns the ID of the event, following the event's link if provided
Examples
iex> Spear.Event.id(%Spear.Event{link: nil, id: "817cf20b-6791-4979-afdd-da4b03e02007", ..)
"817cf20b-6791-4979-afdd-da4b03e02007"
iex> Spear.Event.id(
...> %Spear.Event{
...> link: %Spear.Event{id: "976601b0-3775-442e-b98c-5f56af809402", ..},
...> id: "817cf20b-6791-4979-afdd-da4b03e02007",
...> ..
...> }
...> )
"976601b0-3775-442e-b98c-5f56af809402"
Creates an event struct
This function does not append the event to a stream on its own, but can
provide events to Spear.append/4
which will append events to a stream.
type
is any string used to declare how the event is typed. This is very
arbitrary and may coincide with struct names or may be hard-coded per event.
Options
:id
- (default:Spear.Event.uuid_v4()
) the event's ID. See the section on event IDs below.:content_type
- (default:"application/json"
) the encoding used to turn the event's body into binary data. If the content-type is"application/json"
, the EventStoreDB and Spear (inSpear.Event.from_read_response/2
):custom_metadata
- (default:""
) an event field outside the body meant as a bag for storing custom attributes about an event. Usage of this field is not obligatory: leaving it blank is perfectly normal.
Event IDs
EventStoreDB uses event IDs to provide an idempotency feature. Any event written to the EventStoreDB with an already existing ID will be not be duplicated.
iex> event = Spear.Event.new("grpc-client", %{"languages" => ["typescript", "javascript"], "runtime" => "NodeJS"})
%Spear.Event{
body: %{"languages" => ["typescript", "javascript"], "runtime" => "NodeJS"},
id: "1e654b2a-ff04-4af8-887f-052442edcd83",
metadata: %{content_type: "application/json", custom_metadata: ""},
type: "grpc-client"
}
iex> [event] |> Spear.append(conn, "idempotency_test")
:ok
iex> [event] |> Spear.append(conn, "idempotency_test")
:ok
iex> Spear.stream!(conn, "idempotency_test") |> Enum.to_list()
[
%Spear.Event{
body: %{"languages" => ["typescript", "javascript"], "runtime" => "NodeJS"},
id: "1e654b2a-ff04-4af8-887f-052442edcd83",
metadata: %{
commit_position: 18446744073709551615,
content_type: "application/json",
created: ~U[2021-04-07 21:53:40.395681Z],
custom_metadata: "",
prepare_position: 18446744073709551615,
stream_name: "idempotency_test",
stream_revision: 0
},
type: "grpc-client"
}
]
Event Store DB Event Metadata
All names starting with $ are reserved space for internal use, the most commonly used are the following:
$correlationId
: The application level correlation ID associated with this message.$causationId
: The application level causation ID associated with this message.
In order to use these fields, you must pass them as custom metadata:
iex> custom_metadata = Jason.encode!(%{"$correlationId" => "...", "$causationId" => "..."})
...> Spear.Event.new("my_event", %{"id" => 1}, custom_metadata: custom_metadata)
%Spear.Event{
id: "d77c1abc-0200-4804-81cd-eca726911166",
type: "my_event",
body: %{"id" => 1},
link: nil,
metadata: %{
content_type: "application/json",
custom_metadata: "{"$causationId":"...","$correlationId":"..."}"
}
}
Custom Metadata Format
In order to leverage the EventStoreDB System Projections such as
$by_correlation_id
or JS Projections; you must pass the custom metadata as JSON.
Examples
File.stream!("data.csv")
|> MyCsvParser.parse_stream()
|> Stream.map(fn [id, type, amount] ->
Spear.Event.new("ChargeDeclared",
%{id: id, type: type, amount: amount}
)
end)
|> Spear.append(conn, "ChargesFromCsvs", batch_size: 20)
@spec revision(t()) :: non_neg_integer()
Returns the revision of the event, following the event's link if provided
Examples
iex> Spear.Event.revision(%Spear.Event{link: nil, metadata: %{stream_revision: 1, ..}, ..})
1
iex> Spear.Event.revision(
...> %Spear.Event{
...> link: %Spear.Event{metadata: %{stream_revision: 1, ..}, ..},
...> metadata: %{stream_revision: 0, ..},
...> ..
...> }
...> )
1
@spec to_checkpoint(t()) :: Spear.Filter.Checkpoint.t()
Converts an event into a checkpoint
This is useful when storing stream positions in Spear.subscribe/4
subscriptions to the :all
stream.
to_proposed_message(event, encoder_mapping \\ %{"application/json" => &Jason.encode!/1}, type \\ :append)
View Source (since 0.1.0)@spec to_proposed_message( t(), encoder_mapping :: %{}, type :: :append | :batch_append ) :: tuple()
Converts a Spear.Event
into an append-request record which proposes a new
message
Note that each event must be individually structured as an AppendReq
record in order to be written to an EventStoreDB. The RPC definition for
writing events specifies a stream input, though, so all AppendReq
events
passed to Spear.append/4
will be batched into a single write operation.
This write operation appears to be transactional: any events in a single
call to Spear.append/4
will only be appended if all events can be appended.
rpc Append (stream AppendReq) returns (AppendResp);
These messages are serialized to wire data before being sent to the
EventStoreDB when using Spear.append/4
to write events via protobuf encoding.
encoder_mapping
is a mapping of content-types to 1-arity encode functions.
The default is
%{"application/json" => &Jason.encode!/1}
The Spear.Event.t/0
's .metadata.content_type
value will be searched
in this map. If an encoder is found for that content-type, the event body
will be encoded with the encoding function. If no encoder is found, the
event body will be passed as-is.
To set up an encoder for something like Erlang term format, an encoding map like the following could be used
%{"application/vnd.erlang-term-format" => &:erlang.term_to_binary/1}
In order to disable JSON encoding, pass an empty map %{}
as the
encoder_mapping
Examples
iex> events
[%Spear.Event{}, %Spear.Event{}, ..]
iex> events |> Enum.map(&Spear.Event.to_proposed_message/1)
[{:"event_store.client.streams.AppendReq", ..}, ..]
@spec uuid_v4() :: binary()
Produces a random UUID v4 in human-readable format
Examples
iex> Spear.Event.uuid_v4
"98d3a5e2-ceb4-4a78-8084-97edf9452823"
iex> Spear.Event.uuid_v4
"2629ea4b-d165-45c9-8a2f-92b5e20b894e"
Produces a consistent UUID v4 in human-readable format given any input data structure
This function can be used to generate a consistent UUID for a data structure
of any shape. Under the hood it uses :erlang.phash2/1
to hash the data
structure, which should be portable across many environments.
This function can be taken advantage of to generate consistent event
IDs for the sake of idempotency (see the Event ID section in new/3
for more information). Pass the :id
option to new/3
to override the
default random UUID generation.
Note that it this implementation is naive and not easily portable across
programming languages because of the reliance on :erlang.phash2/1
.
A v5 UUID can be used instead to the same effect with more portability,
however a v5 UUID generator is not included in this library.
Examples
iex> Spear.Event.uuid_v4 %{"foo" => "bar"}
"33323639-3934-4339-b332-363939343339"
iex> Spear.Event.uuid_v4 %{"foo" => "bar"}
"33323639-3934-4339-b332-363939343339"