diff --git a/lib/sanbase/available_metrics/available_metrics.ex b/lib/sanbase/available_metrics/available_metrics.ex index b6334b1a5..89d5e2f1e 100644 --- a/lib/sanbase/available_metrics/available_metrics.ex +++ b/lib/sanbase/available_metrics/available_metrics.ex @@ -68,6 +68,7 @@ defmodule Sanbase.AvailableMetrics do metrics |> Enum.map(fn metric -> {:ok, m} = Sanbase.Metric.metadata(metric) + {:ok, available_selectors} = Sanbase.Metric.available_selectors(metric) %{ metric: m.metric, @@ -79,7 +80,7 @@ defmodule Sanbase.AvailableMetrics do frequency_seconds: Sanbase.DateTimeUtils.str_to_sec(m.min_interval), sanbase_access: "free", sanapi_access: "free", - available_selectors: m.available_selectors, + available_selectors: available_selectors, required_selectors: m.required_selectors, access: Map.get(access_map, metric) } diff --git a/lib/sanbase/clickhouse/founders/founders.ex b/lib/sanbase/clickhouse/founders/founders.ex new file mode 100644 index 000000000..c54bded73 --- /dev/null +++ b/lib/sanbase/clickhouse/founders/founders.ex @@ -0,0 +1,18 @@ +defmodule Sanbase.Clickhouse.Founders do + def get_founders() do + query = get_founders_query() + + Sanbase.ClickhouseRepo.query_transform(query, fn [name, slug] -> + %{name: name, slug: slug} + end) + end + + defp get_founders_query() do + sql = """ + SELECT name, slug + FROM founder_metadata + """ + + Sanbase.Clickhouse.Query.new(sql, %{}) + end +end diff --git a/lib/sanbase/metric/behaviour.ex b/lib/sanbase/metric/behaviour.ex index e05b56d6e..a6a4c62ae 100644 --- a/lib/sanbase/metric/behaviour.ex +++ b/lib/sanbase/metric/behaviour.ex @@ -121,6 +121,8 @@ defmodule Sanbase.Metric.Behaviour do @type available_label_fqns_result :: {:ok, list(String.t())} | {:error, String.t()} + @type available_selectors_result :: {:ok, list(atom())} | {:error, String.t()} + @type has_incomplete_data_result :: boolean() @type complexity_weight_result :: number() diff --git a/lib/sanbase/metric/metric.ex b/lib/sanbase/metric/metric.ex index fbb55b1fa..40f788689 100644 --- a/lib/sanbase/metric/metric.ex +++ b/lib/sanbase/metric/metric.ex @@ -430,6 +430,33 @@ defmodule Sanbase.Metric do end end + @doc ~s""" + Get the list of available selectors for a metric. + This is a separate function as it combines the available selectors from multiple + modules that implement the metric. + Most notably, social metrics from CH are precomptued only for slugs, while the + SocialData.MetricAdapter implements it for text, founders, etc. + """ + @spec available_selectors(metric) :: Type.available_selectors_result() + def available_selectors(metric) do + case get_module(metric, return_all_modules: true) do + nil -> + metric_not_available_error(metric) + + module_or_modules -> + available_selectors = + module_or_modules + |> List.wrap() + |> Enum.flat_map(fn module -> + {:ok, metadata} = apply(module, :metadata, [metric]) + metadata.available_selectors + end) + |> Enum.uniq() + + {:ok, available_selectors} + end + end + @doc ~s""" Get the first datetime for which a given metric is available for a given slug """ @@ -851,7 +878,10 @@ defmodule Sanbase.Metric do end true -> - metric_to_single_module(metric, opts) + case Keyword.get(opts, :return_all_modules, false) do + false -> metric_to_single_module(metric, opts) + true -> Map.get(Helper.metric_to_modules_map(), metric) + end end end diff --git a/lib/sanbase_web/graphql/resolvers/metric/metric_resolver.ex b/lib/sanbase_web/graphql/resolvers/metric/metric_resolver.ex index ee7102695..0cfe9c68f 100644 --- a/lib/sanbase_web/graphql/resolvers/metric/metric_resolver.ex +++ b/lib/sanbase_web/graphql/resolvers/metric/metric_resolver.ex @@ -107,6 +107,29 @@ defmodule SanbaseWeb.Graphql.Resolvers.MetricResolver do end end + def get_available_founders(_root, _args, %{source: %{metric: metric}}) do + with {:ok, selectors} <- Metric.available_selectors(metric) do + case :founders in selectors do + true -> + with {:ok, data} <- Sanbase.Clickhouse.Founders.get_founders() do + slugs = Enum.map(data, & &1.slug) + projects = Sanbase.Project.List.by_slugs(slugs) + slug_to_project_map = Map.new(projects, &{&1.slug, &1}) + + result = + Enum.map(data, fn map -> + Map.put(map, :project, slug_to_project_map[map.slug]) + end) + + {:ok, result} + end + + false -> + {:ok, []} + end + end + end + def get_human_readable_name(_root, _args, %{source: %{metric: metric}}), do: Metric.human_readable_name(metric) @@ -131,6 +154,16 @@ defmodule SanbaseWeb.Graphql.Resolvers.MetricResolver do end end + def get_available_selectors(_root, _args, %{source: %{metric: metric}}) do + case Metric.available_selectors(metric) do + {:ok, selectors} -> + {:ok, selectors} + + {:error, error} -> + {:error, handle_graphql_error("available_selectors", %{metric: metric}, error)} + end + end + def timeseries_data_complexity(_root, args, resolution) do # Explicitly set `child_complexity` to 2 as this would be the # value if both `datetime` and `value` fields are queried. diff --git a/lib/sanbase_web/graphql/schema/types/metric_types.ex b/lib/sanbase_web/graphql/schema/types/metric_types.ex index f4905215c..13f8650a4 100644 --- a/lib/sanbase_web/graphql/schema/types/metric_types.ex +++ b/lib/sanbase_web/graphql/schema/types/metric_types.ex @@ -117,6 +117,11 @@ defmodule SanbaseWeb.Graphql.MetricTypes do value(:table) end + object :founder do + field(:name, non_null(:string)) + field(:project, :project) + end + object :broken_data do field(:from, non_null(:datetime)) field(:to, non_null(:datetime)) @@ -407,7 +412,13 @@ defmodule SanbaseWeb.Graphql.MetricTypes do Every metric has `availableSelectors` in its metadata, showing exactly which of the selectors can be used. """ - field(:available_selectors, list_of(:selector_name)) + field :available_selectors, list_of(:selector_name) do + resolve(&MetricResolver.get_available_selectors/3) + end + + field :available_founders, list_of(:founder) do + cache_resolve(&MetricResolver.get_available_founders/3) + end @desc ~s""" The list of required selectors for the metric. It is used to show the list diff --git a/test/sanbase_web/graphql/metric/api_metric_metadata_test.exs b/test/sanbase_web/graphql/metric/api_metric_metadata_test.exs index 111e5ed94..a7c67241c 100644 --- a/test/sanbase_web/graphql/metric/api_metric_metadata_test.exs +++ b/test/sanbase_web/graphql/metric/api_metric_metadata_test.exs @@ -1,11 +1,64 @@ defmodule SanbaseWeb.Graphql.ApiMetricMetadataTest do use SanbaseWeb.ConnCase, async: false - import Sanbase.Factory, only: [rand_str: 0] + import Sanbase.Factory import SanbaseWeb.Graphql.TestHelpers alias Sanbase.Metric + test "returns data for availableFounders", %{conn: conn} do + metrics_with_founders = + Metric.available_metrics() + |> Enum.filter(fn m -> + {:ok, selectors} = Metric.available_selectors(m) + + :founders in selectors + end) + + insert(:project, %{name: "Ethereum", ticker: "ETH", slug: "ethereum"}) + insert(:project, %{name: "Bitcoin", ticker: "BTC", slug: "bitcoin"}) + + rows = [ + ["Vitalik Buterin", "ethereum"], + ["Satoshi Nakamoto", "bitcoin"] + ] + + query = fn metric -> + """ + { + getMetric(metric: "#{metric}"){ + metadata{ + availableFounders{ name project{ name } } + } + } + } + """ + end + + Sanbase.Mock.prepare_mock2(&Sanbase.ClickhouseRepo.query/2, {:ok, %{rows: rows}}) + |> Sanbase.Mock.run_with_mocks(fn -> + for metric <- metrics_with_founders do + result = + conn + |> post("/graphql", query_skeleton(query.(metric))) + |> json_response(200) + |> get_in(["data", "getMetric", "metadata", "availableFounders"]) + + assert %{"name" => "Vitalik Buterin", "project" => %{"name" => "Ethereum"}} in result + assert %{"name" => "Satoshi Nakamoto", "project" => %{"name" => "Bitcoin"}} in result + end + end) + + result = + conn + |> post("/graphql", query_skeleton(query.("price_usd"))) + |> json_response(200) + |> get_in(["data", "getMetric", "metadata", "availableFounders"]) + + # No founders for metrics without founders in their selectors + assert result == [] + end + test "returns data for all available metric", %{conn: conn} do metrics = Metric.available_metrics() |> Enum.shuffle()