Spear.Event (Spear v0.8.0) View Source

A simplified event struct

This event struct is easier to work with than the protobuf definitions for AppendReq and ReadResp records

Link to this section Summary

Types

t()

A struct representing an EventStoreDB event

Functions

Converts a read-response message to a Spear.Event

Creates an event struct

Converts an event into a checkpoint

Converts a Spear.Event into an append-request struct 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

Link to this section Types

Specs

t() :: %Spear.Event{
  body: term(),
  id: String.t(),
  metadata: map(),
  type: String.t()
}

A struct representing an EventStoreDB event

Spear.Event.t/0s may be created to write to the EventStoreDB with new/3. Spear.Event.t/0s will be lazily mapped into gRPC-compatible structs before being written to the EventStoreDB with to_proposed_message/2.

Spear.Event.t/0s 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.

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"
  }
]
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 to this section Functions

Link to this function

from_read_response(read_response, opts \\ [])

View Source (since 0.1.0)

Specs

from_read_response(tuple(), Keyword.t()) :: t()

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, 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{}, ..]
Link to this function

new(type, body, opts \\ [])

View Source (since 0.1.0)

Specs

new(String.t(), term(), Keyword.t()) :: t()

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 (in Spear.Event.from_read_response/2)
  • :custom_metadata - (default: "") an event field outside of 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"
  }
]

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)
Link to this function

to_checkpoint(event)

View Source (since 0.1.0)

Specs

to_checkpoint(t()) :: Spear.Filter.Checkpoint.t()

Converts an event into a checkpoint

This is useful when storing stream position in Spear.subscribe/4 subscriptions.

Link to this function

to_proposed_message(event, encoder_mapping \\ %{"application/json" => &Jason.encode!/1})

View Source (since 0.1.0)

Specs

to_proposed_message(t(), encoder_mapping :: %{}) :: AppendReq.t()

Converts a Spear.Event into an append-request struct which proposes a new message

Note that each event must be individually structured as an AppendReq message 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.

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)
[%EventStore.Client.Streams.AppendReq{}, ..]

Specs

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"
Link to this function

uuid_v4(term)

View Source (since 0.1.0)

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.

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"