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/shape_definition.ex
defmodule Electric.Client.ShapeDefinition do
@moduledoc """
Struct for defining a shape.
iex> ShapeDefinition.new("items", where: "something = true")
{:ok, %ShapeDefinition{table: "items", where: "something = true", replica: :default}}
"""
alias Electric.Client.Util
@public_keys [:namespace, :table, :columns, :where, :params, :replica]
@derive {Jason.Encoder, only: @public_keys}
@enforce_keys [:table]
defstruct @public_keys ++ [parser: {Electric.Client.ValueMapper, []}]
# only allow things that are trivially convertable to strings
@params_types {:or, [:string, :integer, :float, :boolean]}
@schema_opts [
where: [
type: {:or, [nil, :string]},
required: false,
default: nil,
doc: "Filter the table according to the where clause."
],
columns: [
type: {:or, [nil, {:list, :string}]},
default: nil,
doc:
"List of columns to include in the shape. Must include all primary keys. If `nil` this is equivalent to all columns (`SELECT *`)"
],
namespace: [
type: {:or, [nil, :string]},
required: false,
default: nil,
doc:
"The namespace the table belongs to. If `nil` then Postgres will use whatever schema is the default (usually `public`)."
],
params: [
type: {:or, [nil, {:map, :pos_integer, @params_types}, {:list, @params_types}]},
default: nil,
doc:
"Values of positional parameters in the where clause. These will substitute `$i` placeholder in the where clause."
],
replica: [
type: {:in, [:default, :full]},
default: :default,
doc: """
Modifies the data sent in update and delete change messages.
When set to `:full` the entire row will be sent for updates and deletes,
not just the changed columns.
"""
],
parser: [
type: :mod_arg,
default: {Electric.Client.ValueMapper, []},
doc: """
A `{module, args}` tuple specifying the `Electric.Client.ValueMapper`
implementation to use for mapping values from the sync stream into Elixir
terms.
"""
]
]
@schema NimbleOptions.new!(@schema_opts)
@type t :: %__MODULE__{
namespace: String.t() | nil,
table: String.t(),
columns: [String.t(), ...] | nil,
where: nil | String.t(),
parser: {atom(), term()}
}
@type option :: unquote(NimbleOptions.option_typespec(@schema))
@type options :: [option()]
@doc false
def schema_definition, do: @schema_opts
@spec new(String.t() | keyword()) :: {:ok, t()} | {:error, term()}
def new(table_name) when is_binary(table_name) do
new(table_name, [])
end
def new(opts) when is_list(opts) do
{table, opts} = Keyword.pop(opts, :table, nil)
new(table, opts)
end
@doc """
Create a `ShapeDefinition` for the given `table_name`.
## Options
#{NimbleOptions.docs(@schema)}
"""
@spec new(String.t(), options()) :: {:ok, t()} | {:error, term()}
def new(table_name, opts) when is_binary(table_name) do
with {:ok, opts} <- NimbleOptions.validate(opts, @schema) do
{:ok,
%__MODULE__{
table: table_name,
where: Access.get(opts, :where),
columns: Access.get(opts, :columns),
namespace: Access.get(opts, :namespace),
replica: Access.get(opts, :replica),
parser: Access.get(opts, :parser),
params: Access.get(opts, :params)
}}
end
end
def new(table, _opts) do
{:error, "Missing or invalid table: #{inspect(table)}"}
end
def new!(opts) when is_list(opts) do
opts
|> new()
|> raise_for_invalid!()
end
@spec new!(String.t(), options()) :: t() | no_return()
def new!(table_name, opts \\ []) do
table_name
|> new(opts)
|> raise_for_invalid!()
end
defp raise_for_invalid!(result) do
case result do
{:ok, shape} -> shape
{:error, %NimbleOptions.ValidationError{} = error} -> raise error
{:error, message} when is_binary(message) -> raise ArgumentError, message: message
end
end
def public_keys, do: @public_keys
@doc """
Return a string representation of the shape's table, quoted for use in API URLs.
iex> ShapeDefinition.url_table_name(ShapeDefinition.new!("my_table"))
"my_table"
iex> ShapeDefinition.url_table_name(ShapeDefinition.new!("my_table", namespace: "my_app"))
"my_app.my_table"
iex> ShapeDefinition.url_table_name(ShapeDefinition.new!("my table", namespace: "my app"))
~s["my app"."my table"]
"""
@spec url_table_name(t()) :: String.t()
def url_table_name(%__MODULE__{namespace: nil, table: table}) do
safe_url_name(table)
end
def url_table_name(%__MODULE__{namespace: namespace, table: table}) do
url_table_name(namespace, table)
end
def url_table_name(namespace, table) do
IO.iodata_to_binary([safe_url_name(namespace), ".", safe_url_name(table)])
end
defp safe_url_name(name) do
if name =~ ~r/^[a-z_][a-z0-9_]*$/ do
name
else
quote_table_name(name)
end
end
defp quote_table_name(name) do
IO.iodata_to_binary([
?",
:binary.replace(name, ~s["], ~s[""], [:global]),
?"
])
end
@doc """
Tests if two `%ShapeDefinition{}` instances are equal, ignoring the `parser`
setting.
## Example
iex> {:ok, shape1} = Electric.Client.ShapeDefinition.new("items")
iex> {:ok, shape2} = Electric.Client.ShapeDefinition.new("items")
iex> Electric.Client.ShapeDefinition.matches?(shape1, shape2)
true
iex> {:ok, shape3} = Electric.Client.ShapeDefinition.new("items", where: "something = 'here'")
iex> Electric.Client.ShapeDefinition.matches?(shape1, shape3)
false
"""
@spec matches?(t(), t()) :: boolean()
def matches?(%__MODULE__{} = shape1, %__MODULE__{} = shape2) do
Map.take(shape1, @public_keys) == Map.take(shape2, @public_keys)
end
def matches?(_term1, _term2), do: false
@doc false
@spec params(t(), [{:format, :query | :json | :keyword}]) ::
Electric.Client.Fetch.Request.params()
def params(%__MODULE__{} = shape, opts \\ []) do
%{where: where, columns: columns, params: params, replica: replica} = shape
format = Keyword.get(opts, :format, :query)
shape
|> table_params(format)
|> Util.map_put_if(:where, where, is_binary(where))
|> Util.map_put_if(
:columns,
fn -> params_columns_list(columns, format) end,
is_list(columns)
)
|> maybe_add_replica(replica, format)
|> maybe_add_where_params(:params, params, format)
|> normalize_keys(format)
end
defp table_params(shape, :keyword) do
Map.take(shape, [:table, :namespace])
|> Map.reject(&is_nil(elem(&1, 1)))
end
defp table_params(shape, _format) do
%{table: url_table_name(shape)}
end
defp params_columns_list(columns, :query) when is_list(columns) do
Enum.join(columns, ",")
end
defp params_columns_list(columns, format)
when is_list(columns) and format in [:json, :keyword] do
columns
end
defp maybe_add_replica(input, :default, _format) do
input
end
defp maybe_add_replica(input, replica, :keyword) when is_atom(replica) do
Map.put(input, :replica, replica)
end
defp maybe_add_replica(input, replica, _) when is_atom(replica) do
Map.put(input, :replica, to_string(replica))
end
defp maybe_add_where_params(input, _, nil, _), do: input
defp maybe_add_where_params(input, key, params, format) do
params
|> normalize_params_form()
|> then(&put_param_map(input, key, &1, format))
end
defp normalize_params_form(params) when is_list(params) do
params
|> Enum.with_index(fn elem, index -> {to_string(index + 1), to_string(elem)} end)
|> Map.new()
end
defp normalize_params_form(params) when is_map(params) do
Map.new(params, fn {k, v} -> {to_string(k), to_string(v)} end)
end
defp put_param_map(input, key, params, :query) do
params
|> Map.new(fn {k, v} -> {"#{key}[#{k}]", v} end)
|> Map.merge(input)
end
defp put_param_map(input, key, params, format) when format in [:json, :keyword] do
Map.put(input, key, params)
end
defp normalize_keys(params, :query) do
Map.new(params, fn {k, v} -> {to_string(k), v} end)
end
defp normalize_keys(params, :json) do
params
end
defp normalize_keys(params, :keyword) do
Map.to_list(params)
end
end