Current section

Files

Jump to
phoenix_sync lib phoenix sync.ex
Raw

lib/phoenix/sync.ex

defmodule Phoenix.Sync do
@moduledoc """
Wrappers to ease integration of [Electric’s Postgres syncing
service](https://electric-sql.com) with [Phoenix
applications](https://www.phoenixframework.org/).
There are currently 2 integration modes: [`Phoenix.LiveView`
streams](#module-phoenix-liveview-streams) and [configuration
gateway](#module-configuration-gateway).
## Phoenix.LiveView Streams
`Phoenix.Sync.LiveView.sync_stream/4` integrates with
[`Phoenix.LiveView.stream/4`](https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.html#stream/4)
and provides a live updating collection of items.
## Configuration Gateway
Using `Phoenix.Sync.Plug` you can create endpoints that
return configuration information for your Electric Typescript clients. See
[that module's documentation](`Phoenix.Sync.Plug`) for
more information.
## Installation
Add `phoenix_sync` to your application dependencies:
def deps do
[
{:phoenix_sync, "~> 0.1"}
]
end
## Configuration
In your `config/config.exs` or `config/runtime.exs` you **must** configure the
client for the Electric streaming API:
import Config
config :phoenix_sync, Electric.Client,
# one of `base_url` or `endpoint` is required
base_url: System.get_env("ELECTRIC_URL", "http://localhost:3000"),
# endpoint: System.get_env("ELECTRIC_ENDPOINT", "http://localhost:3000/v1/shape"),
# optional
database_id: System.get_env("ELECTRIC_DATABASE_ID", nil)
See the documentation for [`Electric.Client.new/1`](`Electric.Client.new/1`)
for information on the client configuration.
## Embedding Electric
**TODO**
"""
alias Electric.Client.ShapeDefinition
@shape_keys [:namespace, :where, :columns]
@shape_params @shape_keys |> Enum.map(&to_string/1)
@type shape_specification :: [
unquote(NimbleOptions.option_typespec(Phoenix.Sync.PredefinedShape.schema()))
]
@type shape_definition ::
String.t()
| Ecto.Queryable.t()
| shape_definition()
@type param_override ::
{:namespace, String.t()}
| {:table, String.t()}
| {:where, String.t()}
| {:columns, String.t()}
@type param_overrides :: [param_override()]
defdelegate plug_opts(), to: Phoenix.Sync.Application
@doc false
defdelegate plug_opts(opts), to: Phoenix.Sync.Application
@doc """
Create a new `Electric.Client` instance based on the application config.
To connect your live streams to an externally hosted Electric instance over
HTTP, configure your app with the URL of the Electric server:
# dev.exs
config :phoenix_sync, Electric.Client,
base_url: "http://localhost:3000"
If Electric is installed as a dependency of your app, and you wish to connect
your live streams to this internal application, then you don't need to configure
anything -- `client!/0` will return an `Electric.Client` instance configured to
data directly from the running Electric application.
See [`Electric.Client.new/1`](`Electric.Client.new/1`) for the available
options.
"""
defdelegate client!, to: Phoenix.Sync.Client, as: :new!
@doc """
Use request query parameters to create a `Electric.Client.ShapeDefinition`.
Useful when creating authorization endpoints that validate a user's access to
a specific shape.
## Parameters
### Required
- `table` - the Postgres [table name](https://electric-sql.com/docs/guides/shapes#table)
Note: `table` is not required in the parameters if a `:table` override is set.
### Optional
- `where` - the [Shape's where clause](https://electric-sql.com/docs/guides/shapes#where-clause)
- `columns` - The columns to include in the shape.
- `namespace` - The Postgres namespace (also called `SCHEMA`).
See
[`Electric.Client.ShapeDefinition.new/2`](`Electric.Client.ShapeDefinition.new/2`)
for more details on the parameters.
### Examples
# pass the Plug.Conn struct for a request
iex> Phoenix.Sync.shape_from_params(%Plug.Conn{params: %{"table" => "items", "where" => "visible = true" }})
{:ok, %Electric.Client.ShapeDefinition{table: "items", where: "visible = true"}}
# or a simple parameter map
iex> Phoenix.Sync.shape_from_params(%{"table" => "items", "columns" => "id,name,value" })
{:ok, %Electric.Client.ShapeDefinition{table: "items", columns: ["id", "name", "value"]}}
iex> Phoenix.Sync.shape_from_params(%{"columns" => "id,name,value" })
{:error, "Missing `table` parameter"}
## Overriding Parameter Values
If you want to hard-code some elements of the shape, ignoring the values from
the request, or to set defaults, then use the `overrides` to set specific
values for elements of the shape.
### Examples
iex> Phoenix.Sync.shape_from_params(%{"columns" => "id,name,value"}, table: "things")
{:ok, %Electric.Client.ShapeDefinition{table: "things", columns: ["id", "name", "value"]}}
iex> Phoenix.Sync.shape_from_params(%{"table" => "ignored"}, table: "things")
{:ok, %Electric.Client.ShapeDefinition{table: "things"}}
"""
@spec shape_from_params(Plug.Conn.t() | Plug.Conn.params(), overrides :: param_overrides()) ::
{:ok, Electric.Client.ShapeDefinition.t()} | {:error, String.t()}
def shape_from_params(conn_or_map, overrides \\ [])
def shape_from_params(%Plug.Conn{} = conn, overrides) do
%{params: params} = Plug.Conn.fetch_query_params(conn)
shape_from_params(params, overrides)
end
def shape_from_params(params, overrides) when is_map(params) do
shape_params =
params
|> Map.take(@shape_params)
|> Map.new(fn
{"columns", ""} ->
{:columns, nil}
{"columns", v} when is_binary(v) ->
{:columns, :binary.split(v, ",", [:global, :trim_all])}
{k, v} ->
{String.to_existing_atom(k), v}
end)
if table = Keyword.get(overrides, :table, Map.get(params, "table")) do
ShapeDefinition.new(
table,
Enum.map(@shape_keys, fn k ->
{k, Keyword.get(overrides, k, Map.get(shape_params, k))}
end)
)
else
{:error, "Missing `table` parameter"}
end
end
end