View Source Electric.Phoenix.Plug (Electric Phoenix v0.2.0)
Provides an configuration endpoint for use in your Phoenix applications.
Rather than configuring your Electric Typescript
client directly, you
instead configure a route in your application with a pre-configured
Electric.Client.ShapeDefinition
and then retreive the URL and other
configuration for that shape from your client via a request to your Phoenix
application.
In your Phoenix application, add a route to
Electric.Phoenix.Plug
specifying a particular shape:
defmodule MyAppWeb.Router do
scope "/shapes" do
pipe_through :browser
get "/todos", Electric.Phoenix.Plug,
shape: Electric.Client.shape!("todos", where: "visible = true")
end
end
Then in your client code, you retrieve the shape configuration directly from the Phoenix endpoint:
import { ShapeStream } from '@electric-sql/client'
const endpoint = `https://localhost:4000/shapes/todos`
const response = await fetch(endpoint)
const config = await response.json()
// The returned `config` has all the information you need to subscribe to
// your shape
const stream = new ShapeStream(config)
stream.subscribe(messages => {
// ...
})
You can add additional authentication/authorization for shapes using
Phoenix's
pipelines or
other plug
calls.
Plug.Router
For pure Plug
-based applications, you can use Plug.Router.forward/2
:
defmodule MyRouter do
use Plug.Router
plug :match
plug :dispatch
forward "/shapes/items",
to: Electric.Phoenix.Plug,
shape: Electric.Client.shape!("items")
match _ do
send_resp(conn, 404, "oops")
end
end
Parameter-based shapes
As well as defining fixed-shapes for a particular url, you can request shape configuration using parameters in your request:
defmodule MyAppWeb.Router do
scope "/" do
pipe_through :browser
get "/shape", Electric.Phoenix.Plug, []
end
end
import { ShapeStream } from '@electric-sql/client'
const endpoint = `https://localhost:4000/shape?table=items&namespace=public&where=visible%20%3D%20true`
const response = await fetch(endpoint)
const config = await response.json()
// as before
The parameters are:
table
- The Postgres table name (required).namespace
- The Postgres schema if not specified defaults topublic
.where
- The where clause to filter items from the shape.
Custom Authentication
The Electric.Client
allows for generating authentication headers via it's
authenticator
configuration but if you want to include parameters from the
request in the authentication tokens, for example including the currently
logged in user id, then you can set an authenticator
directly in the Electric.Phoenix.Plug
configuration.
First you must define a function that accepts the %Plug.Conn{}
of the
request, the %Electric.Client.ShapeDefinition{}
of the endpoint and some
(optional) config and returns a map of additional request headers:
defmodule MyAuthModule do
def shape_auth_headers(conn, shape, _opts \\ []) do
user_id = conn.assigns.user_id
signer = Joken.Signer.create("HS256", "my-deep-secret")
claims = %{
user_id: user_id,
table: [shape.namespace, shape.table],
where: shape.where
}
token = Joken.generate_and_sign!(Joken.Config.default_claims(), claims, signer)
%{"authorization" => "Bearer #{token}"}
end
end
Now configure the Shape configuration endpoint to use your authentication function:
forward "/shapes/tasks/:project_id",
to: Electric.Plug,
authenticator: {MyAuthModule, :shape_auth_headers, _opts = []},
shape: [
from(t in Task, where: t.active == true),
project_id: :project_id
]
Or you can use a capture:
forward "/shapes/tasks/:project_id",
to: Electric.Plug,
authenticator: &MyAuthModule.shape_auth_headers/2,
shape: [
from(t in Task, where: t.active == true),
project_id: :project_id
]
Summary
Functions
Callback implementation for Plug.call/2
.
Send the client configuration for a given shape to the browser.
Defines a shape based on a root Ecto
query plus some filters based on the
current request.
Types
@type conn_param_spec() :: param_name() | [{op(), param_name()}]
@type dynamic_shape_param() :: {table_column(), conn_param_spec()} | table_column()
@type dynamic_shape_params() :: [dynamic_shape_param()]
@type op() :: :== | :!= | :> | :< | :>= | :<=
@type param_name() :: atom()
@type table_column() :: atom()
Functions
Callback implementation for Plug.call/2
.
send_configuration(conn, shape_or_queryable, client \\ Electric.Phoenix.client!())
View Source@spec send_configuration( Plug.Conn.t(), Electric.Phoenix.shape_definition(), Client.t() ) :: Plug.Conn.t()
Send the client configuration for a given shape to the browser.
Example
get "/my-shapes/messages" do
user_id = get_session(conn, :user_id)
shape = from(m in Message, where: m.user_id == ^user_id)
Electric.Phoenix.Plug.send_configuration(conn, shape)
end
get "/my-shapes/tasks/:project_id" do
project_id = conn.params["project_id"]
if user_has_access_to_project?(project_id) do
shape = where(Task, project_id: ^project_id)
Electric.Phoenix.Plug.send_configuration(conn, shape)
else
send_resp(conn, :forbidden, "You do not have permission to view project #{project_id}")
end
end
@spec shape!(Electric.Phoenix.shape_definition(), dynamic_shape_params()) :: term()
Defines a shape based on a root Ecto
query plus some filters based on the
current request.
forward "/shapes/tasks/:project_id",
to: Electric.Plug,
shape: Electric.Phoenix.Plug.shape!(
from(t in Task, where: t.active == true),
project_id: :project_id
)
The params
describe the way to build the where
clause on the shape from
the request parameters.
For example, [id: :user_id]
means that the id
column on the table should
match the value of the user_id
parameter, equivalent to:
from(
t in Table,
where: t.id == ^conn.params["user_id"]
)
If both the table column and the request parameter have the same name, then you can just pass the name, so:
Electric.Phoenix.Plug.shape!(Table, [:visible])
is equivalent to:
from(
t in Table,
where: t.visible == ^conn.params["visible"]
)
If you need to match on something other than ==
then you can pass the operator in the params:
Electric.Phoenix.Plug.shape!(Table, size: [>=: :size])
is equivalent to:
from(
t in Table,
where: t.size >= ^conn.params["size"]
)
Instead of calling shape!/2
directly in your route definition, you can just
pass a list of [query | params]
to do the same thing:
forward "/shapes/tasks/:project_id",
to: Electric.Plug,
shape: [
from(t in Task, where: t.active == true),
project_id: :project_id
]