### Plug Router Sync Example Source: https://context7.com/electric-sql/phoenix_sync/llms.txt Demonstrates using `Phoenix.Sync.Router` within a Plug router setup. This allows exposing data shapes without a full Phoenix LiveView context. ```elixir defmodule MyApp.PlugRouter do use Plug.Router, copy_opts_to_assign: :options use Phoenix.Sync.Router plug :match plug :dispatch sync "/shapes/todos", MyApp.Todos.Todo sync "/shapes/active", MyApp.Todos.Todo, where: "completed = false" end ``` -------------------------------- ### Setup DataCase for Phoenix.Sync Sandbox Source: https://context7.com/electric-sql/phoenix_sync/llms.txt Set up the ExUnit CaseTemplate to integrate with the Phoenix.Sync Sandbox. This includes checking out connections and starting the sandbox for tests. ```elixir # test/support/data_case.ex defmodule MyApp.DataCase do use ExUnit.CaseTemplate setup tags do :ok = Ecto.Adapters.SQL.Sandbox.checkout(MyApp.Repo) unless tags[:async] do Ecto.Adapters.SQL.Sandbox.mode(MyApp.Repo, {:shared, self()}) end # Start the Phoenix.Sync sandbox Phoenix.Sync.Sandbox.start!(MyApp.Repo) :ok end end ``` -------------------------------- ### Phoenix.Sync.Controller.sync_render/3 Examples Source: https://context7.com/electric-sql/phoenix_sync/llms.txt Examples demonstrating how to use sync_render/3 in Phoenix controllers for dynamic data synchronization. ```APIDOC ## POST /api/sync/todos ### Description Renders sync shapes from controllers with dynamic query support based on request parameters. ### Method POST ### Endpoint /api/sync/todos ### Parameters #### Query Parameters - **params** (map) - Required - Parameters for dynamic query building. #### Request Body - **conn** (Plug.Conn) - The connection object. - **params** (map) - Request parameters. - **schema_or_query** (module | Ecto.Query | function) - The Ecto schema, query, or a function returning a query. ### Request Example ```elixir def all(conn, params) do sync_render(conn, params, MyApp.Todos.Todo) end def by_status(conn, %{"completed" => completed} = params) do query = from(t in MyApp.Todos.Todo, where: t.completed == ^completed) sync_render(conn, params, query) end def my_todos(%{assigns: %{current_user: user}} = conn, params) do query = from(t in MyApp.Todos.Todo, where: t.user_id == ^user.id) sync_render(conn, params, query) end def project_tasks(conn, %{"project_id" => project_id} = params) do sync_render(conn, params, fn -> task_ids = MyApp.Projects.get_visible_task_ids(project_id) from(t in MyApp.Tasks.Task, where: t.id in ^task_ids) end) end def full_todos(conn, params) do sync_render(conn, params, MyApp.Todos.Todo, replica: :full) end ``` ### Response #### Success Response (200) - **sync_data** (any) - The synchronized data. ### Error Handling - **400 Bad Request**: Invalid parameters or query. - **500 Internal Server Error**: Server error during sync rendering. ``` -------------------------------- ### Start Phoenix.Sync Shape Processes Source: https://context7.com/electric-sql/phoenix_sync/llms.txt Configure the application to start Phoenix.Sync.Shape processes for different Ecto schemas. Supports starting a shape for a full schema or a filtered subset. ```elixir defmodule MyApp.Application do use Application def start(_type, _args) do children = [ MyApp.Repo, # Start a shape process for todos {Phoenix.Sync.Shape, [MyApp.Todos.Todo, name: MyApp.TodoShape]}, # Start a filtered shape {Phoenix.Sync.Shape, [ Ecto.Query.from(t in MyApp.Todos.Todo, where: t.completed == false), name: MyApp.ActiveTodoShape ]} ] Supervisor.start_link(children, strategy: :one_for_one) end end ``` -------------------------------- ### Start Phoenix.Sync Endpoint Source: https://context7.com/electric-sql/phoenix_sync/llms.txt Start the Phoenix.Sync endpoint in your application.ex file by including it in the children list of the supervisor. ```elixir def start(_type, _args) do children = [ MyApp.Repo, {MyAppWeb.Endpoint, phoenix_sync: Phoenix.Sync.plug_opts()} ] opts = [strategy: :one_for_one, name: MyApp.Supervisor] Supervisor.start_link(children, opts) end ``` -------------------------------- ### Phoenix.Sync Installation and Configuration Source: https://context7.com/electric-sql/phoenix_sync/llms.txt Instructions on how to install and configure Phoenix.Sync in your Phoenix application, including embedded and HTTP modes. ```APIDOC ## Installation and Configuration Configure Phoenix.Sync in your application with embedded or HTTP mode for connecting to ElectricSQL. ```elixir # mix.exs - Add dependencies defp deps do [ {:electric, "~> 1.0"}, # Required for embedded mode {:phoenix_sync, "~> 0.6"} ] end # config/config.exs - Embedded mode (Electric runs inside your app) config :phoenix_sync, env: config_env(), mode: :embedded, repo: MyApp.Repo # config/config.exs - HTTP mode (connects to external Electric service) config :phoenix_sync, env: config_env(), mode: :http, url: "https://api.electric-sql.cloud", credentials: [ secret: "your-secret-key", source_id: "your-source-id" # Required for Electric Cloud ] # application.ex - Start the endpoint with Phoenix.Sync def start(_type, _args) do children = [ MyApp.Repo, {MyAppWeb.Endpoint, phoenix_sync: Phoenix.Sync.plug_opts()} ] opts = [strategy: :one_for_one, name: MyApp.Supervisor] Supervisor.start_link(children, opts) end ``` ``` -------------------------------- ### Install Phoenix.Sync in HTTP Mode with Igniter Source: https://github.com/electric-sql/phoenix_sync/blob/main/README.md Install `Phoenix.Sync` in HTTP mode using Igniter, specifying the URL for the external Electric service. ```bash mix igniter.install phoenix_sync --sync-mode http --sync-url "https://api.electric-sql.cloud" ``` -------------------------------- ### Start Local PostgreSQL Database Source: https://github.com/electric-sql/phoenix_sync/blob/main/apps/txid_match/README.md Use this command to create a local PostgreSQL database with an in-memory data directory for testing. ```bash mix txid.postgres up ``` -------------------------------- ### Install Phoenix.Sync in Embedded Mode with Igniter Source: https://github.com/electric-sql/phoenix_sync/blob/main/README.md Use the Igniter mix task to install `Phoenix.Sync` in embedded mode. This command also sets up your `Ecto.Repo` with the sandbox test adapter. ```bash mix igniter.install phoenix_sync --sync-mode embedded ``` -------------------------------- ### Install Phoenix.Sync in Embedded Mode without Sandbox Source: https://github.com/electric-sql/phoenix_sync/blob/main/README.md Install `Phoenix.Sync` in embedded mode using Igniter, opting out of the sandbox test adapter. ```bash mix igniter.install phoenix_sync --sync-mode embedded --no-sync-sandbox ``` -------------------------------- ### Transform Todo Title Example Source: https://context7.com/electric-sql/phoenix_sync/llms.txt An example transformation function that prepends a prefix to the 'title' field of a todo item. ```elixir def transform_todo(msg, prefix) do update_in(msg, ["value", "title"], fn title -> "#{prefix} #{title}" end) end ``` -------------------------------- ### Handle Client Writes with Phoenix.Sync.Writer Source: https://github.com/electric-sql/phoenix_sync/blob/main/README.md Implement a Phoenix controller to handle batched client writes using `Phoenix.Sync.Writer`. This example demonstrates authorizing and validating writes against `Projects.Project` and `Projects.Issue` schemas before applying them atomically using `Ecto.Multi`. ```elixir defmodule MutationController do use Phoenix.Controller, formats: [:json] alias Phoenix.Sync.Writer alias Phoenix.Sync.Writer.Format def mutate(conn, %{"transaction" => transaction} = _params) do user_id = conn.assigns.user_id {:ok, txid, _changes} = Writer.new() |> Writer.allow( Projects.Project, check: reject_invalid_params/1, load: &Projects.load_for_user(&1, user_id), validate: &Projects.Project.changeset/2 ) |> Writer.allow( Projects.Issue, # Use the sensible defaults: # validate: Projects.Issue.changeset/2 # etc. ) |> Writer.apply(transaction, Repo, format: Format.TanstackDB) json(conn, %{txid: txid}) end end ``` -------------------------------- ### Phoenix.Sync Local HTTP Services Configuration Source: https://github.com/electric-sql/phoenix_sync/blob/main/README.md Configure Phoenix.Sync to use HTTP mode with a local Electric service. This setup is useful for testing or when running an HTTP proxy. Include both 'electric' and 'phoenix_sync' dependencies. ```elixir defp deps do [ {:electric, "~> 1.0"}, {:phoenix_sync, "~> 0.6"} ] end config :phoenix_sync, env: config_env(), mode: :http, http: port: 3000, repo: MyApp.Repo, url: "http://localhost:3000" children = [ MyApp.Repo, # ... {MyApp.Endpoint, phoenix_sync: Phoenix.Sync.plug_opts()} ] ``` -------------------------------- ### Sync Filtered Todos in LiveView Source: https://context7.com/electric-sql/phoenix_sync/llms.txt Use `sync_stream/3` with an Ecto query to synchronize a subset of records based on specific criteria. This example syncs todos for a given user ID. ```elixir defmodule MyAppWeb.FilteredTodoLive do use Phoenix.LiveView import Phoenix.Sync.LiveView import Ecto.Query def mount(%{"user_id" => user_id}, _session, socket) do # Sync only todos belonging to a specific user query = from(t in MyApp.Todos.Todo, where: t.user_id == ^user_id) socket = sync_stream(socket, :user_todos, query) {:ok, socket} end def handle_info({:sync, event}, socket) do {:noreply, sync_stream_update(socket, event)} end def render(assigns) do ~H""" """ end end ``` -------------------------------- ### Sync with Where Clause Filter via Router Source: https://context7.com/electric-sql/phoenix_sync/llms.txt Filter records exposed via the router using a `where` option. This example syncs only incomplete todos. ```elixir sync "/incomplete-todos", MyApp.Todos.Todo, where: "completed = false" ``` -------------------------------- ### Stream All Todos from Schema Source: https://context7.com/electric-sql/phoenix_sync/llms.txt Stream all changes from the Todo Ecto schema using Phoenix.Sync.Client.stream/2. This example processes insert, update, and delete operations, and handles the :up_to_date control message. ```elixir defmodule MyApp.SyncConsumer do alias Phoenix.Sync.Client alias MyApp.Todos.Todo def stream_all_todos do # Stream from an Ecto.Schema module stream = Client.stream(Todo) # Process stream events Enum.each(stream, fn %Electric.Client.Message.ChangeMessage{headers: %{operation: :insert}, value: todo} -> IO.puts("New todo: #{todo.title}") %Electric.Client.Message.ChangeMessage{headers: %{operation: :update}, value: todo} -> IO.puts("Updated todo: #{todo.title}") %Electric.Client.Message.ChangeMessage{headers: %{operation: :delete}, value: todo} -> IO.puts("Deleted todo: #{todo.id}") %Electric.Client.Message.ControlMessage{control: :up_to_date} -> IO.puts("Sync is up to date") end) end def stream_incomplete_todos do # Stream from an Ecto.Query with where clause import Ecto.Query from(t in Todo, where: t.completed == false) |> Client.stream() |> Enum.take(10) # Get first 10 incomplete todos end def stream_with_keyword_shape do # Stream using keyword-based shape definition Client.stream( table: "todos", where: "completed = false", columns: ["id", "title", "completed"] ) |> Stream.map(fn msg -> msg.value end) |> Enum.to_list() end end ``` -------------------------------- ### Sync Specific Columns via Router Source: https://context7.com/electric-sql/phoenix_sync/llms.txt Limit the exposed data to specific columns using the `columns` option. This example syncs only the `id` and `title` columns. ```elixir sync "/todo-titles", MyApp.Todos.Todo, columns: ["id", "title"] ``` -------------------------------- ### Add Phoenix.Sync Dependencies Source: https://context7.com/electric-sql/phoenix_sync/llms.txt Add the electric and phoenix_sync dependencies to your mix.exs file. ```elixir defp deps do [ {:electric, "~> 1.0"}, {:phoenix_sync, "~> 0.6"} ] end ``` -------------------------------- ### Add Electric and Phoenix.Sync Dependencies Source: https://github.com/electric-sql/phoenix_sync/blob/main/README.md Manually add the `electric` and `phoenix_sync` libraries to your project's dependencies in `mix.exs` for embedded mode. ```elixir defp deps do [ {:electric, "~> 1.0"}, {:phoenix_sync, "~> 0.6"} ] end ``` -------------------------------- ### Initialize Test Session for Sync Endpoint Source: https://context7.com/electric-sql/phoenix_sync/llms.txt Initialize the test session for a controller test to include the Phoenix.Sync.Sandbox client. This is necessary for testing sync endpoints. ```elixir # test/my_app_web/controllers/todo_controller_test.exs defmodule MyAppWeb.TodoControllerTest do use MyAppWeb.ConnCase setup %{conn: conn} do # Initialize test session with sandbox client conn = Phoenix.Sync.Sandbox.init_test_session(conn, MyApp.Repo) {:ok, conn: conn} end test "sync endpoint returns shape data", %{conn: conn} do # Create test data {:ok, _todo} = MyApp.Todos.create_todo(%{title: "Test"}) conn = get(conn, ~p"/shapes/todos") assert response(conn, 200) end end ``` -------------------------------- ### Sync All Todos in LiveView Source: https://context7.com/electric-sql/phoenix_sync/llms.txt Use `sync_stream/3` in `mount/3` to synchronize all records of a given Ecto schema. Requires `handle_info/2` to process sync events. ```elixir defmodule MyAppWeb.TodoLive do use Phoenix.LiveView import Phoenix.Sync.LiveView import Ecto.Query def mount(_params, _session, socket) do # Sync all todos from the database socket = sync_stream(socket, :todos, MyApp.Todos.Todo) {:ok, assign(socket, show_stream: false)} end # Handle sync events - required callback def handle_info({:sync, event}, socket) do {:noreply, sync_stream_update(socket, event)} end # Handle :loaded event - initial data has been loaded def handle_info({:sync, {:todos, :loaded}}, socket) do {:noreply, assign(socket, :show_stream, true)} end # Handle :live event - stream is now in live sync mode def handle_info({:sync, {:todos, :live}}, socket) do IO.puts("Todos are fully synchronized!") {:noreply, socket} end def render(assigns) do ~H"""
<%= todo.title %> <%= if todo.completed, do: "Done", else: "Pending" %>
""" end end ``` -------------------------------- ### Basic Sync Render with Ecto Schema Source: https://context7.com/electric-sql/phoenix_sync/llms.txt Renders a sync shape for a given Ecto schema. Ensure the schema is imported. ```elixir def all(conn, params) do sync_render(conn, params, MyApp.Todos.Todo) end ``` -------------------------------- ### Configure Phoenix.Sync Sandbox Mode Source: https://context7.com/electric-sql/phoenix_sync/llms.txt Configure the application to use the sandbox adapter for testing Ecto operations. This enables the generation of replication events for testing. ```elixir # config/test.exs config :phoenix_sync, env: config_env(), mode: :sandbox # lib/my_app/repo.ex - Configure repo with sandbox adapter defmodule MyApp.Repo do use Phoenix.Sync.Sandbox.Postgres use Ecto.Repo, otp_app: :my_app, adapter: Phoenix.Sync.Sandbox.Postgres.adapter() end ``` -------------------------------- ### Configure Phoenix.Sync in HTTP Mode Source: https://context7.com/electric-sql/phoenix_sync/llms.txt Configure Phoenix.Sync for HTTP mode in config/config.exs, specifying the environment, mode, URL, and cloud credentials. ```elixir config :phoenix_sync, env: config_env(), mode: :http, url: "https://api.electric-sql.cloud", credentials: [ secret: "your-secret-key", source_id: "your-source-id" # Required for Electric Cloud ] ``` -------------------------------- ### Phoenix.Sync.Router.sync/2 Source: https://context7.com/electric-sql/phoenix_sync/llms.txt Router macro for exposing statically-defined shapes through your Phoenix or Plug router. ```APIDOC ## Phoenix.Sync.Router.sync/2 Router macro for exposing statically-defined shapes through your Phoenix or Plug router. ### Description This macro simplifies the process of defining API endpoints that serve data from your Ecto schemas, allowing for filtering, column selection, and custom transformations. ### Usage in Phoenix Router ```elixir defmodule MyAppWeb.Router do use Phoenix.Router import Phoenix.Sync.Router pipeline :sync do plug :authenticate_user plug :require_sync_permission end scope "/shapes" do pipe_through :sync # Sync entire table sync "/todos", MyApp.Todos.Todo # Sync with where clause filter sync "/incomplete-todos", MyApp.Todos.Todo, where: "completed = false" # Sync specific columns only sync "/todo-titles", MyApp.Todos.Todo, columns: ["id", "title"] # Sync with full replica (includes all columns on updates) sync "/full-todos", MyApp.Todos.Todo, replica: :full # Sync using keyword-based shape definition sync "/projects", table: "projects", where: "archived = false" # Sync with transform function sync "/transformed-todos", MyApp.Todos.Todo, transform: {MyAppWeb.Router, :transform_todo, ["[TODO]"]} end def transform_todo(msg, prefix) do update_in(msg, ["value", "title"], fn title -> "#{prefix} #{title}" end) end end ``` ### Usage in Plug Router ```elixir defmodule MyApp.PlugRouter do use Plug.Router, copy_opts_to_assign: :options use Phoenix.Sync.Router plug :match plug :dispatch sync "/shapes/todos", MyApp.Todos.Todo sync "/shapes/active", MyApp.Todos.Todo, where: "completed = false" end ``` ### Options - `where`: A string representing an Ecto `where` clause. - `columns`: A list of strings specifying which columns to include. - `replica`: Set to `:full` to include all columns on updates. - `table`: Specifies the table name if it differs from the schema name. - `transform`: A tuple `{module, function, args}` for transforming messages. ``` -------------------------------- ### Phoenix.Sync Multi-Environment Configuration Source: https://github.com/electric-sql/phoenix_sync/blob/main/README.md Configure Phoenix.Sync for different environments: embedded in dev, local HTTP in test, and external HTTP in prod. The 'electric' dependency is included only for dev and test environments. ```elixir defp deps do [ {:electric, "~> 1.0", only: [:dev, :test]}, {:phoenix_sync, "~> 0.6"} ] end # config/dev.exs config :phoenix_sync, env: config_env(), mode: :embedded, repo: MyApp.Repo # config/test.esx config :phoenix_sync, env: config_env(), mode: :http, http: port: 3000, repo: MyApp.Repo, url: "http://localhost:3000" # config/prod.exs config :phoenix_sync, mode: :http, url: "https://api.electric-sql.cloud", credentials: secret: "...", # required source_id: "..." # optional, required for Electric Cloud ] children = [ MyApp.Repo, # ... {MyApp.Endpoint, phoenix_sync: Phoenix.Sync.plug_opts()} ] ``` -------------------------------- ### Basic Mutation Handler with TanStack/db Format Source: https://context7.com/electric-sql/phoenix_sync/llms.txt Handles basic mutations using Phoenix.Sync.Writer with the TanStack/db format. Includes authorization checks and validation using Ecto changesets. ```elixir def mutate(conn, %{"transaction" => transaction}) do user_id = conn.assigns.current_user.id {:ok, txid, _changes} = Writer.new() |> Writer.allow( MyApp.Todos.Todo, check: &check_todo_mutation(&1, user_id), load: &load_todo_for_user(&1, user_id), validate: &MyApp.Todos.Todo.changeset/2 ) |> Writer.allow( MyApp.Projects.Project, check: &check_project_mutation(&1, user_id), validate: &MyApp.Projects.Project.changeset/2 ) |> Writer.apply(transaction, MyApp.Repo, format: Format.TanstackDB) json(conn, %{txid: txid}) end ``` -------------------------------- ### Sync Full Replica via Router Source: https://context7.com/electric-sql/phoenix_sync/llms.txt Configure the sync to include all columns on updates by setting `replica: :full`. This ensures complete data is available for all operations. ```elixir sync "/full-todos", MyApp.Todos.Todo, replica: :full ``` -------------------------------- ### Configure Phoenix.Sync for Embedded Mode Source: https://github.com/electric-sql/phoenix_sync/blob/main/README.md Configure `Phoenix.Sync` in `config/config.exs` for embedded mode, specifying the environment, mode, and Ecto repository. ```elixir config :phoenix_sync, env: config_env(), mode: :embedded, repo: MyApp.Repo ``` -------------------------------- ### Add Igniter to Project Dependencies Source: https://github.com/electric-sql/phoenix_sync/blob/main/README.md Include the Igniter library in your project's `mix.exs` file for development and testing purposes. ```elixir {:igniter, "~> 0.6", only: [:dev, :test]} ``` -------------------------------- ### Stream Incomplete Todos with Query Source: https://context7.com/electric-sql/phoenix_sync/llms.txt Stream the first 10 incomplete todos by defining an Ecto query with a where clause and then piping it to Client.stream/1. ```elixir import Ecto.Query from(t in Todo, where: t.completed == false) |> Client.stream() |> Enum.take(10) ``` -------------------------------- ### Sync Render with Shape Options Source: https://context7.com/electric-sql/phoenix_sync/llms.txt Renders a sync shape with additional options, such as specifying the replica mode. This allows for fine-grained control over sync behavior. ```elixir def full_todos(conn, params) do sync_render(conn, params, MyApp.Todos.Todo, replica: :full) end ``` -------------------------------- ### Writing Mutations with TanStack/db Source: https://context7.com/electric-sql/phoenix_sync/llms.txt Ingest optimistic client mutations using the TanStack/db library and fetch API. Monitor the sync stream for transaction IDs to confirm writes. ```typescript async function createTodo(title: string) { const response = await fetch("/api/mutations", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ transaction: [ { type: "insert", syncMetadata: { relation: ["public", "todos"] }, modified: { title, completed: false }, }, ], }), }); const { txid } = await response.json(); // Monitor sync stream for txid to confirm write return txid; } ``` -------------------------------- ### Load Function for Todo Data with Authorization Source: https://context7.com/electric-sql/phoenix_sync/llms.txt Retrieves the original todo record for a mutation, ensuring authorization by filtering by both ID and user ID. This prevents unauthorized data access. ```elixir defp load_todo_for_user(%{"id" => id}, user_id) do import Ecto.Query from(t in MyApp.Todos.Todo, where: t.id == ^id and t.user_id == ^user_id ) |> MyApp.Repo.one() end ``` -------------------------------- ### Render Dynamic Shapes from Controller Source: https://github.com/electric-sql/phoenix_sync/blob/main/README.md Utilize `Phoenix.Sync.Controller.sync_render/3` within a controller to dynamically define and render shapes based on request parameters or session data at runtime. ```elixir defmodule Phoenix.Sync.LiveViewTest.TodoController do use Phoenix.Controller import Phoenix.Sync.Controller import Ecto.Query, only: [from: 2] def show(conn, %{"done" => done} = params) do sync_render(conn, params, from(t in Todos.Todo, where: t.done == ^done)) end def show_mine(%{assigns: %{current_user: user_id}} = conn, params) do sync_render(conn, params, from(t in Todos.Todo, where: t.owner_id == ^user_id)) end end ``` -------------------------------- ### Phoenix.Sync Render Todos with Ecto Schema Source: https://github.com/electric-sql/phoenix_sync/blob/main/README.md Render all todos from the Todos.Todo schema using Phoenix.Sync. This is a simpler way to define a shape when no specific filtering is needed beyond the schema itself. ```elixir sync_render(conn, params, Todos.Todo) ``` -------------------------------- ### Phoenix.Sync Render Todos with Ecto Query Source: https://github.com/electric-sql/phoenix_sync/blob/main/README.md Render incomplete todos using an Ecto.Query with Phoenix.Sync. This method allows for dynamic filtering of data to be synced. ```elixir sync_render(conn, params, from(t in Todos.Todo, where: t.completed == false)) ``` -------------------------------- ### Sync LiveView Stream with Database Source: https://github.com/electric-sql/phoenix_sync/blob/main/README.md Replace `Phoenix.LiveView.stream/3` with `Phoenix.Sync.LiveView.sync_stream/4` to automatically update a LiveView with database changes. The `handle_info` callback is used to process sync events. ```elixir defmodule MyWeb.MyLive do use Phoenix.LiveView import Phoenix.Sync.LiveView def mount(_params, _session, socket) do {:ok, sync_stream(socket, :todos, Todos.Todo)} end def handle_info({:sync, event}, socket) do {:noreply, sync_stream_update(socket, event)} end end ``` -------------------------------- ### Phoenix.Sync HTTP Mode Configuration Source: https://github.com/electric-sql/phoenix_sync/blob/main/README.md Configure Phoenix.Sync to use HTTP mode with an external Electric Cloud service. Ensure the 'phoenix_sync' dependency is added to your mix.exs file. ```elixir defp deps do [ {:phoenix_sync, "~> 0.6"} ] end config :phoenix_sync, env: config_env(), mode: :http, url: "https://api.electric-sql.cloud", credentials: secret: "...", # required source_id: "..." # optional, required for Electric Cloud ] children = [ MyApp.Repo, # ... {MyApp.Endpoint, phoenix_sync: Phoenix.Sync.plug_opts()} ] ``` -------------------------------- ### Test Txid Matching with 64-bit IDs Source: https://github.com/electric-sql/phoenix_sync/blob/main/apps/txid_match/README.md Test transaction ID matching using `txid_current()` which returns 64-bit IDs. This test is expected to fail. ```bash mix txid "txid_current()" ``` -------------------------------- ### Expose Shapes via Phoenix Router Source: https://github.com/electric-sql/phoenix_sync/blob/main/README.md Use the `Phoenix.Sync.Router.sync/2` macro to expose statically defined shapes directly in your Phoenix Router. This allows client connections to go through existing Plug middleware for authentication. ```elixir defmodule MyWeb.Router do use Phoenix.Router import Phoenix.Sync.Router pipeline :sync do plug :my_auth end scope "/shapes" do pipe_through :sync sync "/todos", Todos.Todo end end ``` -------------------------------- ### Sync Entire Table via Router Source: https://context7.com/electric-sql/phoenix_sync/llms.txt Expose an entire Ecto table via the router using `sync/2`. This makes all columns available. ```elixir sync "/todos", MyApp.Todos.Todo ``` -------------------------------- ### Phoenix.Sync Static Shape with Replica Option Source: https://github.com/electric-sql/phoenix_sync/blob/main/README.md Define a static sync shape for incomplete todos, specifying the replica mode as ':full'. This can be combined with a 'where' clause for fine-grained control over data synchronization. ```elixir sync "/incomplete-todos", Todos.Todo, where: "completed = false", replica: :full ``` -------------------------------- ### Phoenix.Sync.Writer API Source: https://context7.com/electric-sql/phoenix_sync/llms.txt Handles write-path sync for ingesting batched optimistic writes from clients with authorization and validation. ```APIDOC ## POST /api/sync/mutate ### Description Handles write-path sync for ingesting batched optimistic writes from clients with authorization and validation. ### Method POST ### Endpoint /api/sync/mutate ### Parameters #### Path Parameters None #### Query Parameters None #### Request Body - **conn** (Plug.Conn) - The connection object. - **transaction** (map) - The transaction data to be applied. ### Request Example ```elixir def mutate(conn, %{"transaction" => transaction}) do user_id = conn.assigns.current_user.id {:ok, txid, _changes} = Writer.new() |> Writer.allow( MyApp.Todos.Todo, check: &check_todo_mutation(&1, user_id), load: &load_todo_for_user(&1, user_id), validate: &MyApp.Todos.Todo.changeset/2 ) |> Writer.allow( MyApp.Projects.Project, check: &check_project_mutation(&1, user_id), validate: &MyApp.Projects.Project.changeset/2 ) |> Writer.apply(transaction, MyApp.Repo, format: Format.TanstackDB) json(conn, %{txid: txid}) end def advanced_mutate(conn, %{"transaction" => transaction}) do user_id = conn.assigns.current_user.id {:ok, txid, _changes} = Writer.new() |> Writer.allow( MyApp.Todos.Todo, check: &check_todo_mutation(&1, user_id), load: fn repo, data -> repo.get(MyApp.Todos.Todo, data["id"]) end, validate: &MyApp.Todos.Todo.changeset/2, insert: [ post_apply: &create_audit_log/3 ], update: [ validate: &MyApp.Todos.Todo.update_changeset/2, pre_apply: &validate_update_permissions/3 ], delete: [ pre_apply: &check_delete_allowed/3 ] ) |> Writer.apply(transaction, MyApp.Repo, format: Format.TanstackDB) json(conn, %{txid: txid}) end ``` ### Response #### Success Response (200) - **txid** (integer) - The transaction ID. ### Error Handling - **400 Bad Request**: Invalid transaction data or validation errors. - **401 Unauthorized**: User not authorized for the operation. - **500 Internal Server Error**: Server error during mutation processing. ``` -------------------------------- ### Subscribe to Phoenix.Sync.Shape Events Source: https://context7.com/electric-sql/phoenix_sync/llms.txt Implement a GenServer to subscribe to synchronization events from a Shape process. Handles insert, update, delete, and synchronization status messages. ```elixir defmodule MyApp.TodoSubscriber do use GenServer alias Phoenix.Sync.Shape def start_link(_) do GenServer.start_link(__MODULE__, nil, name: __MODULE__) end def init(_) do ref = Shape.subscribe(MyApp.TodoShape) {:ok, %{ref: ref}} end # Handle insert events def handle_info({:sync, ref, {:insert, {key, todo}}}, %{ref: ref} = state) do IO.puts("New todo added: #{todo.title}") {:noreply, state} end # Handle update events def handle_info({:sync, ref, {:update, {key, todo}}}, %{ref: ref} = state) do IO.puts("Todo updated: #{todo.title}") {:noreply, state} end # Handle delete events def handle_info({:sync, ref, {:delete, {key, todo}}}, %{ref: ref} = state) do IO.puts("Todo deleted: #{todo.id}") {:noreply, state} end # Handle control messages def handle_info({:sync, ref, :up_to_date}, %{ref: ref} = state) do IO.puts("Shape is fully synchronized") {:noreply, state} end def handle_info({:sync, ref, :must_refetch}, %{ref: ref} = state) do IO.puts("Shape needs refetch") {:noreply, state} end end ``` -------------------------------- ### Create Audit Log Callback Source: https://context7.com/electric-sql/phoenix_sync/llms.txt A callback function used during mutation to create an audit log entry after a todo is successfully inserted. It records the action and the ID of the created record. ```elixir defp create_audit_log(multi, changeset, context) do name = Writer.operation_name(context, :audit_log) Ecto.Multi.insert(multi, name, %MyApp.AuditLog{ action: "todo_created", record_id: changeset.data.id }) end ``` -------------------------------- ### Sync Using Keyword-Based Shape Definition Source: https://context7.com/electric-sql/phoenix_sync/llms.txt Define the data shape using keyword arguments for `table` and `where` clauses, useful for non-Ecto table sources or simpler configurations. ```elixir sync "/projects", table: "projects", where: "archived = false" ``` -------------------------------- ### Check Delete Allowed Callback Source: https://context7.com/electric-sql/phoenix_sync/llms.txt A placeholder callback function for checking delete permissions before a mutation is applied. Currently, it returns the multi-argument unchanged, indicating no custom permission checks are implemented. ```elixir defp check_delete_allowed(multi, _changeset, _context) do multi # Add delete permission checks end ``` -------------------------------- ### Advanced Mutation Handler with Per-Operation Callbacks Source: https://context7.com/electric-sql/phoenix_sync/llms.txt Handles advanced mutations with per-operation callbacks for insert, update, and delete. This allows for custom logic like audit logging and permission checks before applying changes. ```elixir def advanced_mutate(conn, %{"transaction" => transaction}) do user_id = conn.assigns.current_user.id {:ok, txid, _changes} = Writer.new() |> Writer.allow( MyApp.Todos.Todo, check: &check_todo_mutation(&1, user_id), load: fn repo, data -> repo.get(MyApp.Todos.Todo, data["id"]) end, validate: &MyApp.Todos.Todo.changeset/2, insert: [ post_apply: &create_audit_log/3 ], update: [ validate: &MyApp.Todos.Todo.update_changeset/2, pre_apply: &validate_update_permissions/3 ], delete: [ pre_apply: &check_delete_allowed/3 ] ) |> Writer.apply(transaction, MyApp.Repo, format: Format.TanstackDB) json(conn, %{txid: txid}) end ``` -------------------------------- ### Test Sync Stream with Insert Events Source: https://context7.com/electric-sql/phoenix_sync/llms.txt Write tests to verify that sync_stream receives insert events when records are created within the sandboxed connection. Asserts that a sync event is received. ```elixir # test/my_app/todos_sync_test.exs defmodule MyApp.TodosSyncTest do use MyApp.DataCase import Phoenix.Sync.LiveView test "sync_stream receives insert events" do # Create a socket and sync stream {:ok, socket} = Phoenix.LiveView.Socket.build_socket() socket = sync_stream(socket, :todos, MyApp.Todos.Todo) # Insert a record - the sandbox will emit sync events {:ok, todo} = MyApp.Todos.create_todo(%{title: "Test Todo"}) # Verify the sync event was received assert_receive {:sync, _event} end test "allowing external processes" do # Allow a worker process to access the sandboxed connection worker_pid = Process.whereis(MyApp.Worker) Phoenix.Sync.Sandbox.allow(MyApp.Repo, self(), worker_pid) # Worker can now insert and trigger sync events GenServer.call(MyApp.Worker, {:create_todo, %{title: "Worker Todo"}}) end end ``` -------------------------------- ### Dynamic Sync Render Based on Request Parameters Source: https://context7.com/electric-sql/phoenix_sync/llms.txt Renders a sync shape dynamically based on request parameters, allowing for filtered queries. The query is constructed using Ecto.Query. ```elixir def by_status(conn, %{"completed" => completed} = params) do query = from(t in MyApp.Todos.Todo, where: t.completed == ^completed) sync_render(conn, params, query) end ``` -------------------------------- ### Phoenix.Sync Static Shape with Where Clause Source: https://github.com/electric-sql/phoenix_sync/blob/main/README.md Define a static sync shape in the router for incomplete todos using an Ecto.Schema and a 'where' clause. This avoids recompilation issues associated with Ecto.Query structs in routers. ```elixir sync "/incomplete-todos", Todos.Todo, where: "completed = false" ``` -------------------------------- ### Consuming Phoenix.Sync Shapes with TypeScript Client Source: https://context7.com/electric-sql/phoenix_sync/llms.txt Integrate Phoenix.Sync shapes into frontend applications using the Electric TypeScript client. Create shape streams, subscribe to changes, and handle authentication. ```typescript import { Shape, ShapeStream } from "@electric-sql/client"; // Create a shape stream const stream = new ShapeStream({ url: `/shapes/todos`, }); const shape = new Shape(stream); // Subscribe to changes shape.subscribe((data) => { console.log("Todos updated:", data); }); // With authentication const authenticatedStream = new ShapeStream({ url: `/shapes/my-todos`, headers: { Authorization: `Bearer ${authToken}`, }, }); ``` -------------------------------- ### Test Txid Matching with 32-bit IDs Source: https://github.com/electric-sql/phoenix_sync/blob/main/apps/txid_match/README.md Test transaction ID matching using `pg_current_xact_id()::xid` which returns 32-bit IDs. This test is expected to pass. ```bash mix txid "pg_current_xact_id()::xid" ``` -------------------------------- ### Phoenix.Sync.Client.stream/2 Source: https://context7.com/electric-sql/phoenix_sync/llms.txt API for converting an Ecto query or schema into an Elixir Stream for consuming sync events programmatically. ```APIDOC ## Phoenix.Sync.Client.stream/2 Converts an Ecto query or schema into an Elixir Stream for consuming sync events programmatically. ### Example Usage ```elixir defmodule MyApp.SyncConsumer do alias Phoenix.Sync.Client alias MyApp.Todos.Todo def stream_all_todos do # Stream from an Ecto.Schema module stream = Client.stream(Todo) # Process stream events Enum.each(stream, fn %Electric.Client.Message.ChangeMessage{headers: %{operation: :insert}, value: todo} -> IO.puts("New todo: #{todo.title}") %Electric.Client.Message.ChangeMessage{headers: %{operation: :update}, value: todo} -> IO.puts("Updated todo: #{todo.title}") %Electric.Client.Message.ChangeMessage{headers: %{operation: :delete}, value: todo} -> IO.puts("Deleted todo: #{todo.id}") %Electric.Client.Message.ControlMessage{control: :up_to_date} -> IO.puts("Sync is up to date") end) end def stream_incomplete_todos do # Stream from an Ecto.Query with where clause import Ecto.Query from(t in Todo, where: t.completed == false) |> Client.stream() |> Enum.take(10) # Get first 10 incomplete todos end def stream_with_keyword_shape do # Stream using keyword-based shape definition Client.stream( table: "todos", where: "completed = false", columns: ["id", "title", "completed"] ) |> Stream.map(fn msg -> msg.value end) |> Enum.to_list() end end ``` ``` -------------------------------- ### Interruptible Sync Render for Dynamic Queries Source: https://context7.com/electric-sql/phoenix_sync/llms.txt Supports interruptible sync shapes for dynamic queries that may change. The provided function is re-evaluated on interrupt, allowing for real-time updates. ```elixir def project_tasks(conn, %{"project_id" => project_id} = params) do sync_render(conn, params, fn -> # This function is re-evaluated on interrupt task_ids = MyApp.Projects.get_visible_task_ids(project_id) from(t in MyApp.Tasks.Task, where: t.id in ^task_ids) end) end ``` -------------------------------- ### Stream Ecto Query to Elixir Stream Source: https://github.com/electric-sql/phoenix_sync/blob/main/README.md Use `Phoenix.Sync.Client.stream/2` to convert an Ecto.Query into an Elixir Stream for low-level usage. ```elixir stream = Phoenix.Sync.Client.stream(Todos.Todo) stream = Ecto.Query.from(t in Todos.Todo, where: t.completed == false) |> Phoenix.Sync.Client.stream() ``` -------------------------------- ### Validate Update Permissions Callback Source: https://context7.com/electric-sql/phoenix_sync/llms.txt A placeholder callback function for validating update permissions before a mutation is applied. Currently, it returns the multi-argument unchanged, indicating no custom validation logic is implemented. ```elixir defp validate_update_permissions(multi, _changeset, _context) do multi # Add custom validation logic end ``` -------------------------------- ### Stop Local PostgreSQL Database Source: https://github.com/electric-sql/phoenix_sync/blob/main/apps/txid_match/README.md Use this command to stop the PostgreSQL server and release the in-memory RAM disk after testing. ```bash mix txid.postgres down ``` -------------------------------- ### Sync with Transform Function via Router Source: https://context7.com/electric-sql/phoenix_sync/llms.txt Apply a custom transformation function to the data before it's exposed. The function receives the message and a prefix, allowing modification of the data. ```elixir sync "/transformed-todos", MyApp.Todos.Todo, transform: {MyAppWeb.Router, :transform_todo, ["[TODO]"]} ``` -------------------------------- ### React Integration with useShape Hook Source: https://context7.com/electric-sql/phoenix_sync/llms.txt Utilize the useShape hook from @electric-sql/react for seamless integration of Phoenix.Sync shapes within React components. Handles loading states and errors. ```typescript import { useShape } from "@electric-sql/react"; function TodoList() { const { data, isLoading, error } = useShape({ url: `/shapes/todos`, }); if (isLoading) return
Loading...
; if (error) return
Error: {error.message}
; return ( ); } ``` -------------------------------- ### User-Specific Data Sync Render Source: https://context7.com/electric-sql/phoenix_sync/llms.txt Renders sync shapes for user-specific data by accessing the current user from the connection assigns. Ensures data is scoped to the authenticated user. ```elixir def my_todos(%{assigns: %{current_user: user}} = conn, params) do query = from(t in MyApp.Todos.Todo, where: t.user_id == ^user.id) sync_render(conn, params, query) end ``` -------------------------------- ### Stream Todos with Keyword Shape Definition Source: https://context7.com/electric-sql/phoenix_sync/llms.txt Stream todos using a keyword-based shape definition, specifying the table, where clause, and desired columns. The stream is then mapped to extract the value and converted to a list. ```elixir Client.stream( table: "todos", where: "completed = false", columns: ["id", "title", "completed"] ) |> Stream.map(fn msg -> msg.value end) |> Enum.to_list() ``` -------------------------------- ### Consume Shapes with Electric TypeScript Client Source: https://github.com/electric-sql/phoenix_sync/blob/main/README.md Use the Electric TypeScript client to subscribe to data changes from a specified URL. The callback function is invoked every time the data is updated. ```typescript import { Shape, ShapeStream } from "@electric-sql/client"; const stream = new ShapeStream({ url: `/shapes/todos`, }); const shape = new Shape(stream); // The callback runs every time the data changes. shape.subscribe((data) => console.log(data)); ``` -------------------------------- ### Access Data from Phoenix.Sync.Shape Source: https://context7.com/electric-sql/phoenix_sync/llms.txt Provides functions to retrieve data from a Shape process as a list, map, or stream. Supports transforming keys and values during retrieval. ```elixir defmodule MyApp.TodoCache do alias Phoenix.Sync.Shape # Get all data as a list def list_todos do Shape.to_list(MyApp.TodoShape) # Returns: [{key, %Todo{}}, ...] end # Get all values without keys def list_todo_values do Shape.to_list(MyApp.TodoShape, keys: false) # Returns: [%Todo{}, ...] end # Get data as a map def todos_map do Shape.to_map(MyApp.TodoShape) # Returns: %{key => %Todo{}, ...} end # Transform keys and values def todos_by_id do Shape.to_map(MyApp.TodoShape, fn {_key, %{id: id, title: title}} -> {id, title} end) # Returns: %{1 => "My Todo", 2 => "Another Todo", ...} end # Find a specific item def find_todo_by_title(title) do Shape.find(MyApp.TodoShape, fn todo -> todo.title == title end) end # Get a lazy stream def stream_todos do Shape.stream(MyApp.TodoShape) |> Enum.into(%{}) end end ```