diff --git a/.changeset/thick-boxes-argue.md b/.changeset/thick-boxes-argue.md new file mode 100644 index 0000000000..93cfd2ce02 --- /dev/null +++ b/.changeset/thick-boxes-argue.md @@ -0,0 +1,5 @@ +--- +"@core/elixir-client": patch +--- + +Derive Jason.Encoder for Client.ShapeDefinition diff --git a/examples/gatekeeper-auth/api/config/dev.exs b/examples/gatekeeper-auth/api/config/dev.exs index e65a05276b..2cbc330e20 100644 --- a/examples/gatekeeper-auth/api/config/dev.exs +++ b/examples/gatekeeper-auth/api/config/dev.exs @@ -29,7 +29,7 @@ config :api, ApiWeb.Endpoint, # Configure the Electric.Phoenix.Plug to route electric client requests # via this application's `GET /proxy/v1/shape` endpoint. -config :electric_phoenix, electric_url: "http://localhost:#{port}/proxy" +config :electric_phoenix, Electric.Client, base_url: "http://localhost:#{port}/proxy" # Do not include metadata nor timestamps in development logs config :logger, :console, format: "[$level] $message\n" diff --git a/examples/gatekeeper-auth/api/config/runtime.exs b/examples/gatekeeper-auth/api/config/runtime.exs index 22d83a2a82..ed41bf7cb0 100644 --- a/examples/gatekeeper-auth/api/config/runtime.exs +++ b/examples/gatekeeper-auth/api/config/runtime.exs @@ -69,5 +69,5 @@ if config_env() == :prod do default_proxy_url = URI.parse("https://#{host}:#{port}/proxy") |> URI.to_string() proxy_url = System.get_env("ELECTRIC_PROXY_URL") || default_proxy_url - config :electric_phoenix, electric_url: proxy_url + config :electric_phoenix, Electric.Client, base_url: proxy_url end diff --git a/examples/gatekeeper-auth/api/config/test.exs b/examples/gatekeeper-auth/api/config/test.exs index 0f2ada3a51..91fe8452ad 100644 --- a/examples/gatekeeper-auth/api/config/test.exs +++ b/examples/gatekeeper-auth/api/config/test.exs @@ -27,7 +27,7 @@ config :api, ApiWeb.Endpoint, # Configure the Electric.Phoenix.Plug to route electric client requests # via this application's `GET /proxy/v1/shape` endpoint. -config :electric_phoenix, electric_url: "http://localhost:#{port}/proxy" +config :electric_phoenix, Electric.Client, base_url: "http://localhost:#{port}/proxy" # Print only warnings and errors during test config :logger, level: :warning diff --git a/examples/gatekeeper-auth/api/lib/api_web/authenticator.ex b/examples/gatekeeper-auth/api/lib/api_web/authenticator.ex index 2c37a5b97e..dd97e692e6 100644 --- a/examples/gatekeeper-auth/api/lib/api_web/authenticator.ex +++ b/examples/gatekeeper-auth/api/lib/api_web/authenticator.ex @@ -1,22 +1,18 @@ defmodule ApiWeb.Authenticator do @moduledoc """ - `Electric.Client.Authenticator` implementation that generates - and validates tokens. + Functions for that generating and validating tokens. """ + alias Api.Token - alias Electric.Client - @behaviour Client.Authenticator @header_name "Authorization" - def authenticate_shape(shape, _config) do + # We configure our `Electric.Phoenix.Plug` handler with this function as the + # `authenticator` function in order to return a signed token to the client. + def authentication_headers(_conn, shape) do %{@header_name => "Bearer #{Token.generate(shape)}"} end - def authenticate_request(request, _config) do - request - end - def authorise(shape, request_headers) do header_map = Enum.into(request_headers, %{}) header_key = String.downcase(@header_name) @@ -28,27 +24,4 @@ defmodule ApiWeb.Authenticator do {:error, :missing} end end - - # Provides an `Electric.Client` that uses our `Authenticator` - # implementation to generate signed auth tokens. - # - # This is configured in `./router.ex` to work with the - # `Electric.Phoenix.Plug`: - # - # post "/:table", Electric.Phoenix.Plug, client: &Authenticator.client/0 - # - # Because `client/0` returns a client that's configured to use our - # `ApiWeb.Authenticator`, then `ApiWeb.Authenticator.authenticate_shape/2` - # will be called to generate an auth header that's included in the - # response data that the Electric.Phoenix.Plug returns to the client. - # - # I.e.: we basically tie into the `Gateway.Plug` machinery to use our - # `Authenticator` to generate and return a signed token to the client. - def client do - base_url = Application.fetch_env!(:electric_phoenix, :electric_url) - - {:ok, client} = Client.new(base_url: base_url, authenticator: {__MODULE__, []}) - - client - end end diff --git a/examples/gatekeeper-auth/api/lib/api_web/router.ex b/examples/gatekeeper-auth/api/lib/api_web/router.ex index a43d8ca1e7..e9129021dd 100644 --- a/examples/gatekeeper-auth/api/lib/api_web/router.ex +++ b/examples/gatekeeper-auth/api/lib/api_web/router.ex @@ -28,7 +28,8 @@ defmodule ApiWeb.Router do scope "/gatekeeper" do pipe_through :gatekeeper - post "/:table", Electric.Phoenix.Plug, client: &Authenticator.client/0 + post "/:table", Electric.Phoenix.Plug, + authenticator: &Authenticator.authentication_headers/2 end # The proxy endpoint at `GET /proxy/v1/shape` proxies the request to an diff --git a/examples/gatekeeper-auth/api/mix.exs b/examples/gatekeeper-auth/api/mix.exs index e2e17d6508..75a3975a2e 100644 --- a/examples/gatekeeper-auth/api/mix.exs +++ b/examples/gatekeeper-auth/api/mix.exs @@ -26,7 +26,7 @@ defmodule Api.MixProject do defp deps do [ {:bandit, "~> 1.5"}, - {:electric_phoenix, "~> 0.1.3"}, + {:electric_phoenix, ">= 0.2.0-rc-1"}, {:ecto_sql, "~> 3.10"}, {:jason, "~> 1.4"}, {:joken, "~> 2.6"}, diff --git a/examples/gatekeeper-auth/api/mix.lock b/examples/gatekeeper-auth/api/mix.lock index f7305c1dc1..ead03d4738 100644 --- a/examples/gatekeeper-auth/api/mix.lock +++ b/examples/gatekeeper-auth/api/mix.lock @@ -6,8 +6,8 @@ "dns_cluster": {:hex, :dns_cluster, "0.1.3", "0bc20a2c88ed6cc494f2964075c359f8c2d00e1bf25518a6a6c7fd277c9b0c66", [:mix], [], "hexpm", "46cb7c4a1b3e52c7ad4cbe33ca5079fbde4840dedeafca2baf77996c2da1bc33"}, "ecto": {:hex, :ecto, "3.12.4", "267c94d9f2969e6acc4dd5e3e3af5b05cdae89a4d549925f3008b2b7eb0b93c3", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "ef04e4101688a67d061e1b10d7bc1fbf00d1d13c17eef08b71d070ff9188f747"}, "ecto_sql": {:hex, :ecto_sql, "3.12.1", "c0d0d60e85d9ff4631f12bafa454bc392ce8b9ec83531a412c12a0d415a3a4d0", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.12", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.7", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.19 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "aff5b958a899762c5f09028c847569f7dfb9cc9d63bdb8133bff8a5546de6bf5"}, - "electric_client": {:hex, :electric_client, "0.1.2", "1b4b2c3f53a44adaf98a648da21569325338a123ec8f00b7d26c6e3c3583ac94", [:mix], [{:ecto_sql, "~> 3.12", [hex: :ecto_sql, repo: "hexpm", optional: true]}, {:gen_stage, "~> 1.2", [hex: :gen_stage, repo: "hexpm", optional: true]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:nimble_options, "~> 1.1", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:req, "~> 0.5", [hex: :req, repo: "hexpm", optional: false]}], "hexpm", "fde191b8ce7c70c44ef12a821210699222ceba1951f73c3c4e7cff8c1d5c0294"}, - "electric_phoenix": {:hex, :electric_phoenix, "0.1.3", "5d3fef07343b3b8611ec73d1a712ccb84ae0adb2f6a7d73a44acf57ae7efab65", [:mix], [{:ecto_sql, "~> 3.10", [hex: :ecto_sql, repo: "hexpm", optional: true]}, {:electric_client, "~> 0.1.2", [hex: :electric_client, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:nimble_options, "~> 1.1", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.20", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "2b5dc8cd2d16db102035e3e59a7874ee00631aa7f6c7180f368c022884cc05fd"}, + "electric_client": {:hex, :electric_client, "0.2.1-rc-2", "3087196645d7cb0e2f7c7b77d84828c363f9069eb1340de5ac8181dc137aaa3f", [:mix], [{:ecto_sql, "~> 3.12", [hex: :ecto_sql, repo: "hexpm", optional: true]}, {:gen_stage, "~> 1.2", [hex: :gen_stage, repo: "hexpm", optional: true]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:nimble_options, "~> 1.1", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:req, "~> 0.5", [hex: :req, repo: "hexpm", optional: false]}], "hexpm", "a9dbb5e2ddb6e82e99109f226524e30980f443c6a23295ebb08ae7f675525409"}, + "electric_phoenix": {:hex, :electric_phoenix, "0.2.0-rc-1", "c7f8f7b0db274f22b3189634e1b4e443d4cb1482527ec691ee6a0516f1eefbc2", [:mix], [{:ecto_sql, "~> 3.10", [hex: :ecto_sql, repo: "hexpm", optional: true]}, {:electric_client, "> 0.2.0", [hex: :electric_client, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:nimble_options, "~> 1.1", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.20", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "1fca7706043a9559520c44b43b7b81f62748d7513a2a1c7664b65a81919d24fd"}, "finch": {:hex, :finch, "0.19.0", "c644641491ea854fc5c1bbaef36bfc764e3f08e7185e1f084e35e0672241b76d", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.6.2 or ~> 1.7", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 1.1", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "fc5324ce209125d1e2fa0fcd2634601c52a787aff1cd33ee833664a5af4ea2b6"}, "hpax": {:hex, :hpax, "1.0.0", "28dcf54509fe2152a3d040e4e3df5b265dcb6cb532029ecbacf4ce52caea3fd2", [:mix], [], "hexpm", "7f1314731d711e2ca5fdc7fd361296593fc2542570b3105595bb0bc6d0fad601"}, "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"}, diff --git a/examples/gatekeeper-auth/api/test/api_web/authenticator_test.exs b/examples/gatekeeper-auth/api/test/api_web/authenticator_test.exs index 4f19a2a3dd..ee9504bf84 100644 --- a/examples/gatekeeper-auth/api/test/api_web/authenticator_test.exs +++ b/examples/gatekeeper-auth/api/test/api_web/authenticator_test.exs @@ -9,13 +9,13 @@ defmodule ApiWeb.AuthenticatorTest do {:ok, shape} = Shape.from(%{"table" => "foo"}) assert %{"Authorization" => "Bearer " <> _token} = - Authenticator.authenticate_shape(shape, nil) + Authenticator.authentication_headers(nil, shape) end test "validate token" do {:ok, shape} = Shape.from(%{"table" => "foo"}) - headers = Authenticator.authenticate_shape(shape, nil) + headers = Authenticator.authentication_headers(nil, shape) assert Authenticator.authorise(shape, headers) end @@ -26,7 +26,7 @@ defmodule ApiWeb.AuthenticatorTest do "where" => "value IS NOT NULL" }) - headers = Authenticator.authenticate_shape(shape, nil) + headers = Authenticator.authentication_headers(nil, shape) assert Authenticator.authorise(shape, headers) end end diff --git a/examples/gatekeeper-auth/docker-compose.yaml b/examples/gatekeeper-auth/docker-compose.yaml index d5346baa35..59f85001ca 100644 --- a/examples/gatekeeper-auth/docker-compose.yaml +++ b/examples/gatekeeper-auth/docker-compose.yaml @@ -34,7 +34,7 @@ services: AUTH_SECRET: "NFL5*0Bc#9U6E@tnmC&E7SUN6GwHfLmY" DATABASE_URL: "postgresql://postgres:password@postgres:5432/electric?sslmode=disable" ELECTRIC_URL: "http://electric:3000" - ELECTRIC_PROXY_URL: "${ELECTRIC_PROXY_URL:-http://localhost:3000/proxy}" + ELECTRIC_PROXY_URL: "${ELECTRIC_PROXY_URL:-http://localhost:4000/proxy}" PHX_HOST: "localhost" PHX_PORT: 4000 PHX_SCHEME: "http" diff --git a/examples/phoenix-liveview/config/dev.exs b/examples/phoenix-liveview/config/dev.exs index 4146273445..574b463020 100644 --- a/examples/phoenix-liveview/config/dev.exs +++ b/examples/phoenix-liveview/config/dev.exs @@ -83,5 +83,5 @@ config :phoenix_live_view, # Enable helpful, but potentially expensive runtime checks enable_expensive_runtime_checks: true -config :electric_phoenix, - electric_url: System.get_env("ELECTRIC_URL", "http://localhost:3000/") +config :electric_phoenix, Electric.Client, + base_url: System.get_env("ELECTRIC_URL", "http://localhost:3000/") diff --git a/examples/phoenix-liveview/config/runtime.exs b/examples/phoenix-liveview/config/runtime.exs index 6d81adcb8c..0ba9eb0330 100644 --- a/examples/phoenix-liveview/config/runtime.exs +++ b/examples/phoenix-liveview/config/runtime.exs @@ -96,4 +96,7 @@ if config_env() == :prod do # force_ssl: [hsts: true] # # Check `Plug.SSL` for all available options in `force_ssl`. + + config :electric_phoenix, Electric.Client, + base_url: System.get_env("ELECTRIC_URL") || raise("ELECTRIC_URL environment variable not set") end diff --git a/examples/phoenix-liveview/mix.exs b/examples/phoenix-liveview/mix.exs index 0dd2bf9022..e484bcd341 100644 --- a/examples/phoenix-liveview/mix.exs +++ b/examples/phoenix-liveview/mix.exs @@ -56,7 +56,7 @@ defmodule Electric.PhoenixExample.MixProject do {:jason, "~> 1.2"}, {:dns_cluster, "~> 0.1.1"}, {:bandit, "~> 1.5"}, - {:electric_phoenix, "~> 0.1.3"} + {:electric_phoenix, ">= 0.2.0-rc-1"} ] end diff --git a/examples/phoenix-liveview/mix.lock b/examples/phoenix-liveview/mix.lock index dca13c3951..1c8b06d8ec 100644 --- a/examples/phoenix-liveview/mix.lock +++ b/examples/phoenix-liveview/mix.lock @@ -6,8 +6,8 @@ "dns_cluster": {:hex, :dns_cluster, "0.1.3", "0bc20a2c88ed6cc494f2964075c359f8c2d00e1bf25518a6a6c7fd277c9b0c66", [:mix], [], "hexpm", "46cb7c4a1b3e52c7ad4cbe33ca5079fbde4840dedeafca2baf77996c2da1bc33"}, "ecto": {:hex, :ecto, "3.12.4", "267c94d9f2969e6acc4dd5e3e3af5b05cdae89a4d549925f3008b2b7eb0b93c3", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "ef04e4101688a67d061e1b10d7bc1fbf00d1d13c17eef08b71d070ff9188f747"}, "ecto_sql": {:hex, :ecto_sql, "3.12.1", "c0d0d60e85d9ff4631f12bafa454bc392ce8b9ec83531a412c12a0d415a3a4d0", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.12", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.7", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.19 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "aff5b958a899762c5f09028c847569f7dfb9cc9d63bdb8133bff8a5546de6bf5"}, - "electric_client": {:hex, :electric_client, "0.1.2", "1b4b2c3f53a44adaf98a648da21569325338a123ec8f00b7d26c6e3c3583ac94", [:mix], [{:ecto_sql, "~> 3.12", [hex: :ecto_sql, repo: "hexpm", optional: true]}, {:gen_stage, "~> 1.2", [hex: :gen_stage, repo: "hexpm", optional: true]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:nimble_options, "~> 1.1", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:req, "~> 0.5", [hex: :req, repo: "hexpm", optional: false]}], "hexpm", "fde191b8ce7c70c44ef12a821210699222ceba1951f73c3c4e7cff8c1d5c0294"}, - "electric_phoenix": {:hex, :electric_phoenix, "0.1.3", "5d3fef07343b3b8611ec73d1a712ccb84ae0adb2f6a7d73a44acf57ae7efab65", [:mix], [{:ecto_sql, "~> 3.10", [hex: :ecto_sql, repo: "hexpm", optional: true]}, {:electric_client, "~> 0.1.2", [hex: :electric_client, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:nimble_options, "~> 1.1", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.20", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "2b5dc8cd2d16db102035e3e59a7874ee00631aa7f6c7180f368c022884cc05fd"}, + "electric_client": {:hex, :electric_client, "0.2.1-rc-1", "008db5e2b31fdee7ed53f115c7e1a6b602901a6abea1f79adb9c8a24bcfb4d11", [:mix], [{:ecto_sql, "~> 3.12", [hex: :ecto_sql, repo: "hexpm", optional: true]}, {:gen_stage, "~> 1.2", [hex: :gen_stage, repo: "hexpm", optional: true]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:nimble_options, "~> 1.1", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:req, "~> 0.5", [hex: :req, repo: "hexpm", optional: false]}], "hexpm", "29142b78dfe8c29e0fff9ba5df4c75c9f41bd0d708ce0dfff530cb50117226a6"}, + "electric_phoenix": {:hex, :electric_phoenix, "0.2.0-rc-1", "c7f8f7b0db274f22b3189634e1b4e443d4cb1482527ec691ee6a0516f1eefbc2", [:mix], [{:ecto_sql, "~> 3.10", [hex: :ecto_sql, repo: "hexpm", optional: true]}, {:electric_client, "> 0.2.0", [hex: :electric_client, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:nimble_options, "~> 1.1", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.20", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "1fca7706043a9559520c44b43b7b81f62748d7513a2a1c7664b65a81919d24fd"}, "esbuild": {:hex, :esbuild, "0.8.1", "0cbf919f0eccb136d2eeef0df49c4acf55336de864e63594adcea3814f3edf41", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "25fc876a67c13cb0a776e7b5d7974851556baeda2085296c14ab48555ea7560f"}, "expo": {:hex, :expo, "1.1.0", "f7b9ed7fb5745ebe1eeedf3d6f29226c5dd52897ac67c0f8af62a07e661e5c75", [:mix], [], "hexpm", "fbadf93f4700fb44c331362177bdca9eeb8097e8b0ef525c9cc501cb9917c960"}, "file_system": {:hex, :file_system, "1.0.1", "79e8ceaddb0416f8b8cd02a0127bdbababe7bf4a23d2a395b983c1f8b3f73edd", [:mix], [], "hexpm", "4414d1f38863ddf9120720cd976fce5bdde8e91d8283353f0e31850fa89feb9e"}, diff --git a/packages/elixir-client/lib/electric/client/shape_definition.ex b/packages/elixir-client/lib/electric/client/shape_definition.ex index 91fd4282cb..91cb58c9df 100644 --- a/packages/elixir-client/lib/electric/client/shape_definition.ex +++ b/packages/elixir-client/lib/electric/client/shape_definition.ex @@ -10,6 +10,8 @@ defmodule Electric.Client.ShapeDefinition do @enforce_keys [:table] + @derive {Jason.Encoder, except: [:parser]} + defstruct [:namespace, :table, :columns, :where, parser: {Electric.Client.ValueMapper, []}] @schema NimbleOptions.new!(