View Source Electric.Phoenix.Plug (Electric Phoenix v0.1.3)

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 to public.
  • where - The where clause to filter items from the shape.

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()
Link to this type

dynamic_shape_params()

View Source
@type dynamic_shape_params() :: [dynamic_shape_param()]
@type op() :: :== | :!= | :> | :< | :>= | :<=
@type param_name() :: atom()
@type table_column() :: atom()

Functions

Callback implementation for Plug.call/2.

Link to this function

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

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
  ]