Current section
Files
Jump to
Current section
Files
lib/phoenix/sync/client.ex
defmodule Phoenix.Sync.Client do
@moduledoc """
Low level Elixir client. Converts an `Ecto.Query` into an Elixir `Stream`:
```elixir
stream = Phoenix.Sync.Client.stream(Todos.Todo)
stream =
Ecto.Query.from(t in Todos.Todo, where: t.completed == false)
|> Phoenix.Sync.Client.stream()
```
"""
alias Phoenix.Sync.PredefinedShape
@doc """
Create a new sync client based on the `:phoenix_sync` configuration.
"""
def new do
Phoenix.Sync.Application.config() |> new()
end
def new(nil) do
new()
end
@doc """
Create a sync client using the given options.
If the integration mode is set to `:embedded` and Electric is installed
then this will configure the client to retrieve data using the internal
Elixir APIs.
For the `:http` mode, then you must also configure a URL specifying an
Electric API server:
config :phoenix_sync,
mode: :http,
url: "https://api.electric-sql.cloud"
This client can then generate streams for use in your Elixir applications:
{:ok, client} = Phoenix.Sync.Client.new()
stream = Electric.Client.stream(client, Todos.Todo)
for msg <- stream, do: IO.inspect(msg)
Alternatively use `stream/1` which wraps this functionality.
"""
def new(opts) do
{adapter, env} = Phoenix.Sync.Application.adapter_env(opts)
apply(adapter, :client, [env, opts])
end
@doc """
Create a new sync client based on the application configuration or raise if
the config is invalid.
client = Phoenix.Sync.Client.new!()
See `new/0`.
"""
def new! do
case new() do
{:ok, client} ->
client
{:error, reason} ->
raise RuntimeError, message: "Invalid client configuration: #{reason}"
end
end
@doc """
Create a new sync client based on the given opts or raise if
the config is invalid.
client = Phoenix.Sync.Client.new!(mode: :embedded)
See `new/1`.
"""
def new!(opts) do
case new(opts) do
{:ok, client} ->
client
{:error, reason} ->
raise RuntimeError, message: "Invalid client configuration: #{reason}"
end
end
@doc """
Return a sync stream for the given shape.
## Examples
# stream updates for the Todo schema
stream = Phoenix.Sync.Client.stream(MyApp.Todos.Todo)
# stream the results of an ecto query
stream = Phoenix.Sync.Client.stream(from(t in MyApp.Todos.Todo, where: t.completed == true))
# create a stream based on a shape definition
stream = Phoenix.Sync.Client.stream(
table: "todos",
where: "completed = false",
columns: ["id", "title"]
)
# once you have a stream, consume it as usual
Enum.each(stream, &IO.inspect/1)
## Ecto vs keyword shapes
Streams defined using an Ecto query or schema will return data wrapped in
the appropriate schema struct, with values cast to the appropriate
Elixir/Ecto types, rather than raw column data in the form `%{"column_name"
=> "column_value"}`.
"""
@spec stream(Phoenix.Sync.shape_definition(), Electric.Client.stream_options()) :: Enum.t()
def stream(shape, stream_opts \\ [])
def stream(table, stream_opts) when is_binary(table) and is_list(stream_opts) do
stream(Keyword.put(stream_opts, :table, table), [])
end
def stream(shape, []) when is_list(shape) do
{client, shape} = Keyword.pop_lazy(shape, :client, &new!/0)
{shape, shape_stream_opts} = resolve_shape(shape, [])
Electric.Client.stream(client, shape, shape_stream_opts)
end
def stream(shape, stream_opts) when not is_list(shape) and is_list(stream_opts) do
{client, stream_opts} = Keyword.pop_lazy(stream_opts, :client, &new!/0)
{shape, shape_stream_opts} = resolve_shape(shape, stream_opts)
Electric.Client.stream(client, shape, shape_stream_opts)
end
defp resolve_shape(shape, stream_opts) do
shape
|> PredefinedShape.new!(stream_opts)
|> Phoenix.Sync.PredefinedShape.to_stream_params()
end
end