Packages
electric_client
0.10.3
0.10.3
0.10.2
0.10.1
0.10.1-beta-1
0.10.0
0.9.5-beta-1
0.9.4
0.9.4-beta-1
0.9.3
0.9.2
0.9.1
0.9.0
0.8.3
0.8.3-beta-1
0.8.2
0.8.1
0.8.0
0.8.0-beta-1
0.7.3
0.7.2
0.7.1
0.7.0
0.6.5
0.6.5-beta-5
0.6.5-beta-4
0.6.5-beta-3
0.6.5-beta-2
0.6.5-beta-1
0.6.4
0.6.3
0.6.2
0.6.1
0.6.0
0.5.0
0.5.0-beta-1
0.4.1
0.4.0
0.3.2
0.3.1
0.3.0
0.3.0-beta.4
0.3.0-beta.3
0.3.0-beta.2
0.2.6-pre-1
retired
0.2.6-beta.1
0.2.6-beta.0
0.2.5
0.2.4
0.2.4-pre-8
0.2.4-pre-7
0.2.4-pre-6
0.2.4-pre-5
0.2.4-pre-4
0.2.4-pre-3
0.2.4-pre-2
0.2.4-pre-1
0.2.3
0.2.3-rc-1
0.2.2
0.2.2-rc-1
0.2.1
0.2.1-rc-3
0.2.1-rc-2
0.2.1-rc-1
0.2.0
0.1.2
0.1.1
0.1.0
0.1.0-dev-9
0.1.0-dev-8
0.1.0-dev-7
0.1.0-dev-6
0.1.0-dev-5
0.1.0-dev-4
0.1.0-dev-3
0.1.0-dev-2
0.1.0-dev-17
0.1.0-dev-16
0.1.0-dev-15
0.1.0-dev-14
0.1.0-dev-13
0.1.0-dev-12
0.1.0-dev-11
0.1.0-dev-10
0.1.0-dev
Elixir client for ElectricSQL
Current section
Files
Jump to
Current section
Files
lib/electric/client/value_mapper.ex
defmodule Electric.Client.ValueMapper do
@moduledoc """
A behaviour for mapping the `value` fields of [`Message.ChangeMessage`](`Electric.Client.Message.ChangeMessage`) messages
from the shape stream.
This requires implementing a single function `c:for_schema/2`.
Electric sends schema information with every response. For example the schema
of the `foo` table from the [Electric
Quickstart](https://electric-sql.com/docs/quickstart) is
%{
id: %{type: "int4", pk_index: 0, not_null: true},
name: %{type: "varchar", max_length: 255},
value: %{type: "float8"}
}
The `c:for_schema/2` function receives this schema as the first argument and it
must return a 1-arity function that will map the value structs received from
Electric to the desired format.
E.g
# the schema for the `foo` table
schema = %{
id: %{type: "int4", pk_index: 0, not_null: true},
name: %{type: "varchar", max_length: 255},
value: %{type: "float8"}
}
# get the mapper function for this schema
mapper_fun = Electric.Client.ValueMapper.for_schema(schema, opts = [])
value = %{
"id" => "1",
"name" => "James",
"value" => "45.6"
}
# the mapping parses the integer and float values to their respective
# Elixir/Erlang types.
mapper_fun.(value)
%{
"id" => 1,
"name" => "James",
"value" => 45.6
}
The current implementation only handles integer and float values. Every other
column is left as a binary.
"""
alias Electric.Client
alias Electric.Client.Message
@type opts :: term()
@type mapper_fun :: (Message.ChangeMessage.value() -> term())
@doc """
Given the schema information passed from the electric server should return a
1-arity function that takes a values map, which is a map of column names to
string values, and returns a version with the values cast to some appropriate
Elixir type.
"""
@callback for_schema(Client.schema(), opts()) :: mapper_fun()
@behaviour __MODULE__
@impl __MODULE__
def for_schema(schema, _opts) do
mapping = build_mapping(schema)
&map_values(&1, mapping)
end
defp map_values(values, mapping) do
Map.new(values, fn {k, v} ->
case mapping do
%{^k => fun} -> {k, fun.(v)}
_ -> {k, v}
end
end)
end
defp build_mapping(schema) when is_map(schema) do
Map.new(schema, &column_mapping/1)
end
defp column_mapping({column_name, %{type: type} = _column_info}) do
{to_string(column_name), map_type(type)}
end
# This is a deep hole to dig. Rather than attempt to provide mappings of all
# PG types here I'm only doing ints and floats, mostly as a POC, and relying
# on the Ecto integration to do the real work.
#
# If we have time and or a requirement to expand type support here then it's
# fairly trivial to do but a lot of work.
#
# Note that the values we receive here have come out of postgres, so the
# validation requirements are light-to-none.
defp map_type(<<"int", n::binary-1>>) when n in ~w(2 4 8) do
fn
nil -> nil
val -> String.to_integer(val)
end
end
defp map_type(<<"float", n::binary-1>>) when n in ~w(4 8) do
fn
nil ->
nil
int when is_integer(int) ->
:erlang.float(int)
val ->
{f, ""} = Float.parse(val)
f
end
end
defp map_type(_type) do
& &1
end
end