From a0572647a187be37cabd97e7c7d5c1c9cc69fddb Mon Sep 17 00:00:00 2001 From: Wen Bo Xie <5532241+w3b6x9@users.noreply.github.com> Date: Mon, 21 Dec 2020 08:14:35 -0500 Subject: [PATCH] fix: Update Configuration to include realtime config settings (#92) --- server/lib/realtime/configuration.ex | 128 ++++++++++++------ server/test/realtime/configuration_test.exs | 69 +++++++++- server/test/support/example_config.json | 54 +++++--- server/test/support/example_empty_config.json | 1 + 4 files changed, 184 insertions(+), 68 deletions(-) create mode 100644 server/test/support/example_empty_config.json diff --git a/server/lib/realtime/configuration.ex b/server/lib/realtime/configuration.ex index 2acd630e2..afb80849d 100644 --- a/server/lib/realtime/configuration.ex +++ b/server/lib/realtime/configuration.ex @@ -1,79 +1,119 @@ defmodule Realtime.Configuration do + defmodule(ConfigurationFileError, + do: defexception(message: "configuration file is missing required attribute(s)") + ) + defmodule(WebhookEndpoint, do: defstruct([:endpoint])) defmodule(Webhook, do: defstruct([:event, :relation, :config])) defmodule(Realtime, do: defstruct([:events, :relation])) - defmodule(Configuration, do: defstruct([:webhooks, :realtime])) + defmodule(Configuration, do: defstruct(webhooks: [], realtime: [])) + + @events ["INSERT", "UPDATE", "DELETE"] @doc """ - Load Configuration from a json file. + + Load and convert configuration settings from a json file. + """ - def from_json_file(nil) do + def from_json_file(filename) when is_binary(filename) and filename != "" do {:ok, - %Configuration{ - webhooks: [], - realtime: [ + filename + |> File.read!() + |> Jason.decode!() + |> convert_raw_config!()} + end + + def from_json_file(_) do + {:ok, + Map.put( + %Configuration{}, + :realtime, + [ %Realtime{ relation: "*", - events: ["INSERT", "UPDATE", "DELETE"] + events: @events }, %Realtime{ relation: "*:*", - events: ["INSERT", "UPDATE", "DELETE"] + events: @events }, %Realtime{ relation: "*:*:*", - events: ["INSERT", "UPDATE", "DELETE"] + events: @events } ] - }} + )} end - def from_json_file(filename) do - with {:ok, body} <- File.read(filename), do: from_json(body) + defp convert_raw_config!(raw_config) do + default_config = %Configuration{} + + default_config + |> Map.keys() + |> Enum.reduce(default_config, fn connector_type, acc_config -> + convert_connector_config!(acc_config, raw_config, connector_type) + end) end - @doc """ - Load Configuration from a json string. - """ - def from_json(body) do - with {:ok, raw_config} <- Jason.decode(body), do: to_configuration(raw_config) + defp convert_connector_config!(%Configuration{} = config, %{"webhooks" => webhooks}, :webhooks) + when is_list(webhooks) do + webhooks = + Enum.map(webhooks, fn webhook -> + case Kernel.get_in(webhook, ["config", "endpoint"]) do + endpoint when is_binary(endpoint) and endpoint != "" -> + config_endpoint = %WebhookEndpoint{endpoint: endpoint} + # default to all events + event = Map.get(webhook, "event", "*") + # default to all relations + relation = Map.get(webhook, "relation", "*") + + %Webhook{event: event, relation: relation, config: config_endpoint} + + _ -> + raise ConfigurationFileError + end + end) + + %Configuration{config | webhooks: webhooks} end - defp to_configuration(config) do - with {:ok, raw_webhooks} <- Map.fetch(config, "webhooks"), - {:ok, webhooks} <- to_webhooks_configuration(raw_webhooks) - do - {:ok, %Configuration{webhooks: webhooks}} - end + defp convert_connector_config!(%Configuration{}, %{"webhooks" => _}, :webhooks) do + raise ConfigurationFileError end - defp to_webhooks_configuration(config) do - to_webhooks_configuration(config, []) + defp convert_connector_config!(%Configuration{} = config, _, :webhooks) do + config end - defp to_webhooks_configuration([], acc), do: {:ok, Enum.reverse(acc)} - defp to_webhooks_configuration([config | rest], acc) do - case to_webhook_configuration(config) do - {:ok, config} -> to_webhooks_configuration(rest, [config | acc]) - _ -> :error - end + + defp convert_connector_config!( + %Configuration{} = config, + %{"realtime" => subscribers}, + :realtime + ) + when is_list(subscribers) do + subscribers = + Enum.map(subscribers, fn subscriber -> + with {:ok, relation} <- Map.fetch(subscriber, "relation"), + {:ok, events} <- Map.fetch(subscriber, "events") do + %Realtime{relation: relation, events: events} + else + _ -> raise ConfigurationFileError + end + end) + + %Configuration{config | realtime: subscribers} end + defp convert_connector_config!(%Configuration{}, %{"realtime" => _}, :realtime) do + raise ConfigurationFileError + end - defp to_webhook_configuration(config) do - with {:ok, raw_endpoint} <- Map.fetch(config, "config"), - {:ok, endpoint} <- to_webhook_endpoint_configuration(raw_endpoint) - do - event = Map.get(config, "events", "*") # default to all events - relation = Map.get(config, "relation", "*") # default to all relations - {:ok, %Webhook{event: event, relation: relation, config: endpoint}} - end + defp convert_connector_config!(%Configuration{} = config, _, :realtime) do + config end - defp to_webhook_endpoint_configuration(config) do - with {:ok, endpoint} <- Map.fetch(config, "endpoint") - do - {:ok, %WebhookEndpoint{endpoint: endpoint}} - end + defp convert_connector_config!(%Configuration{} = config, _, :__struct__) do + config end end diff --git a/server/test/realtime/configuration_test.exs b/server/test/realtime/configuration_test.exs index 372ab358b..215398d43 100644 --- a/server/test/realtime/configuration_test.exs +++ b/server/test/realtime/configuration_test.exs @@ -2,15 +2,72 @@ defmodule Realtime.ConfigurationTest do use ExUnit.Case alias Realtime.Configuration + alias Realtime.Configuration.{Realtime, Webhook, WebhookEndpoint} - test "parse a configuration from a json file" do + test "Realtime.Configuration.from_json_file/1 when filename not given" do + {:ok, config} = Configuration.from_json_file(nil) + + assert config == %Configuration.Configuration{ + webhooks: [], + realtime: [ + %Realtime{ + relation: "*", + events: ["INSERT", "UPDATE", "DELETE"] + }, + %Realtime{ + relation: "*:*", + events: ["INSERT", "UPDATE", "DELETE"] + }, + %Realtime{ + relation: "*:*:*", + events: ["INSERT", "UPDATE", "DELETE"] + } + ] + } + end + + test "Realtime.Configuration.from_json_file/1 when file contains an empty JSON object" do + filename = Path.expand("../support/example_empty_config.json", __DIR__) + + {:ok, config} = Configuration.from_json_file(filename) + + assert config == %Configuration.Configuration{webhooks: [], realtime: []} + end + + test "Realtime.Configuration.from_json_file/1 when file contains configuration attributes" do filename = Path.expand("../support/example_config.json", __DIR__) + {:ok, config} = Configuration.from_json_file(filename) - assert Enum.count(config.webhooks) == 3 - webhook = List.first(config.webhooks) - assert webhook.event == "*" - assert webhook.relation == "*" - assert String.starts_with?(webhook.config.endpoint, "https://") + assert config == %Configuration.Configuration{ + realtime: [ + %Realtime{events: ["INSERT", "UPDATE"], relation: "public"}, + %Realtime{events: ["INSERT"], relation: "public:users"}, + %Realtime{events: ["DELETE"], relation: "public:users.id=eq.1"} + ], + webhooks: [ + %Webhook{ + config: %WebhookEndpoint{ + endpoint: "https://webhook.site/44e4457a-77ae-4758-8d52-17efdf484853" + }, + event: "*", + relation: "*" + }, + %Webhook{ + config: %WebhookEndpoint{ + endpoint: "https://webhook.site/44e4457a-77ae-4758-8d52-17efdf484853" + }, + event: "INSERT", + relation: "public" + }, + %Webhook{ + config: %WebhookEndpoint{ + endpoint: "https://webhook.site/44e4457a-77ae-4758-8d52-17efdf484853" + }, + event: "UPDATE", + relation: "public:users.id=eq.2" + } + ] + } end end diff --git a/server/test/support/example_config.json b/server/test/support/example_config.json index 831eec889..236aedfc8 100644 --- a/server/test/support/example_config.json +++ b/server/test/support/example_config.json @@ -1,20 +1,38 @@ { - "webhooks": [{ - "event": "*", - "config": { - "endpoint": "https://webhook.site/44e4457a-77ae-4758-8d52-17efdf484853" - } - }, { - "event": "INSERT", - "relation": "public", - "config": { - "endpoint": "https://webhook.site/44e4457a-77ae-4758-8d52-17efdf484853" - } - }, { - "event": "UPDATE", - "relation": "public:users.id=eq.2", - "config": { - "endpoint": "https://webhook.site/44e4457a-77ae-4758-8d52-17efdf484853" - } - }] + "webhooks": [ + { + "event": "*", + "config": { + "endpoint": "https://webhook.site/44e4457a-77ae-4758-8d52-17efdf484853" + } + }, + { + "event": "INSERT", + "relation": "public", + "config": { + "endpoint": "https://webhook.site/44e4457a-77ae-4758-8d52-17efdf484853" + } + }, + { + "event": "UPDATE", + "relation": "public:users.id=eq.2", + "config": { + "endpoint": "https://webhook.site/44e4457a-77ae-4758-8d52-17efdf484853" + } + } + ], + "realtime": [ + { + "events": ["INSERT", "UPDATE"], + "relation": "public" + }, + { + "events": ["INSERT"], + "relation": "public:users" + }, + { + "events": ["DELETE"], + "relation": "public:users.id=eq.1" + } + ] } diff --git a/server/test/support/example_empty_config.json b/server/test/support/example_empty_config.json new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/server/test/support/example_empty_config.json @@ -0,0 +1 @@ +{}