Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

absinthe persistent term backend #4356

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions lib/sanbase/application/application.ex
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,9 @@ defmodule Sanbase.Application do
"""
def prepended_children(container_type) do
[
# This child is required so the Absinthe uses
# the persistent_term backend
{Absinthe.Schema, SanbaseWeb.Graphql.Schema},
start_in_and_if(
fn ->
%{
Expand Down Expand Up @@ -333,6 +336,8 @@ defmodule Sanbase.Application do

# Process that starts test-only deps
start_in(Sanbase.TestSetupService, [:test]),

# Start the Event Bus
Sanbase.EventBus.children()
]
|> List.flatten()
Expand Down
3 changes: 0 additions & 3 deletions lib/sanbase/application/web.ex
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,6 @@ defmodule Sanbase.Application.Web do
id: Sanbase.ApiCallLimitMutex
),

# Start GraphQL subscriptions
{Absinthe.Subscription, SanbaseWeb.Endpoint},

# Start the graphQL in-memory cache
SanbaseWeb.Graphql.Cache.child_spec(
id: :graphql_api_cache,
Expand Down
145 changes: 104 additions & 41 deletions lib/sanbase/billing/api_info.ex
Original file line number Diff line number Diff line change
@@ -1,18 +1,31 @@
defmodule Sanbase.Billing.ApiInfo do
@moduledoc ~s"""
@type query_or_argument_tuple ::
{:query, Atom.t()} | {:metric, String.t()} | {:signal, String.t()}

"""
def query_type() do
case :persistent_term.get(:sanbase_absinthe_schema_query_type, :not_stored) do
:not_stored ->
data = Absinthe.Schema.lookup_type(SanbaseWeb.Graphql.Schema, :query)
:persistent_term.put(:sanbase_absinthe_schema_query_type, data)
data

# NOTE: In case of compile time error for reasons like wrong import_types and
# similar, the error will be not include the right place where it errored. In this
# case replace the @query_type with the commented one - it has high chances for the
# proper error location to be revealed
# @query_type %{fields: %{}}
@query_type Absinthe.Schema.lookup_type(SanbaseWeb.Graphql.Schema, :query)
@fields @query_type.fields |> Map.keys()
data ->
data
end
end

@type query_or_argument_tuple ::
{:query, Atom.t()} | {:metric, String.t()} | {:signal, String.t()}
def query_type_fields() do
case :persistent_term.get(:sanbase_absinthe_schema_query_type_fields, :not_stored) do
:not_stored ->
query_type = query_type()
fields = query_type.fields |> Map.keys()
:persistent_term.put(:sanbase_absinthe_schema_query_type_fields, fields)
fields

fields ->
fields
end
end

@typedoc """
Key is one of "SANAPI" or "SANBASE". Value is one of "FREE", "PRO", etc.
Expand All @@ -25,15 +38,25 @@ defmodule Sanbase.Billing.ApiInfo do
"""
@spec min_plan_map() :: %{query_or_argument_tuple => product_min_plan_map}
def min_plan_map() do
# Metadata looks like this:
# meta(access: :restricted, min_plan: [sanapi: "PRO", sanbase: "FREE"])
query_min_plan_map = get_query_min_plan_map()
metric_min_plan_map = get_metric_min_plan_map()
signal_min_plan_map = get_signal_min_plan_map()

query_min_plan_map
|> Map.merge(metric_min_plan_map)
|> Map.merge(signal_min_plan_map)
case :persistent_term.get(:absinthe_min_plan_map, :not_stored) do
:not_stored ->
# Metadata looks like this:
# meta(access: :restricted, min_plan: [sanapi: "PRO", sanbase: "FREE"])
query_min_plan_map = get_query_min_plan_map()
metric_min_plan_map = get_metric_min_plan_map()
signal_min_plan_map = get_signal_min_plan_map()

data =
query_min_plan_map
|> Map.merge(metric_min_plan_map)
|> Map.merge(signal_min_plan_map)

:persistent_term.put(:absinthe_min_plan_map, data)
data

data ->
data
end
end

@doc ~s"""
Expand All @@ -45,34 +68,50 @@ defmodule Sanbase.Billing.ApiInfo do
when is_list(fields) and is_list(values) and length(fields) == length(values) do
field_value_pairs = Enum.zip(fields, values)

Enum.filter(@fields, fn f ->
Enum.filter(query_type_fields(), fn f ->
Enum.all?(field_value_pairs, fn {field, value} ->
Map.get(@query_type.fields, f) |> Absinthe.Type.meta(field) == value
Map.get(query_type().fields, f) |> Absinthe.Type.meta(field) == value
end)
end)
end

def get_all_with_access_level(level) do
# List of {:query, atom()}
queries_with_access_level =
get_queries_with_access_level(level)
|> Enum.map(&{:query, &1})
case :persistent_term.get({:absinthe_get_all_with_access_level, level}, :not_stored) do
:not_stored ->
data = do_get_all_with_access_level(level) |> MapSet.new()
:persistent_term.put({:absinthe_get_all_with_access_level, level}, data)
data

data ->
data
end
end

# List of {:signal, String.t()}
signals_with_access_level =
Sanbase.Signal.access_map()
|> get_with_access_level(level)
|> Enum.map(&{:signal, &1})
def get_all_with_any_access_level() do
case :persistent_term.get(:absinthe_get_all_with_any_access_level, :not_stored) do
:not_stored ->
free = do_get_all_with_access_level(:free)
restricted = do_get_all_with_access_level(:restricted)
data = free ++ restricted

# List of {:metric, String.t()}
metrics_with_access_level =
Sanbase.Metric.access_map()
|> get_with_access_level(level)
|> Enum.map(&{:metric, &1})
:persistent_term.put(:absinthe_get_all_with_any_access_level, data)
data

queries_with_access_level ++
signals_with_access_level ++
metrics_with_access_level
data ->
data
end
end

def get_all_with_access_level_mapset(level) do
case :persistent_term.get({:absinthe_get_all_with_access_level_mapset, level}, :not_stored) do
:not_stored ->
data = do_get_all_with_access_level(level) |> MapSet.new()
:persistent_term.put({:absinthe_get_all_with_access_level_mapset, level}, data)
data

data ->
data
end
end

def get_with_access_level(access_map, level) do
Expand All @@ -95,9 +134,33 @@ defmodule Sanbase.Billing.ApiInfo do
end

# Private functions

defp do_get_all_with_access_level(level) do
# List of {:query, atom()}
queries_with_access_level =
get_queries_with_access_level(level)
|> Enum.map(&{:query, &1})

# List of {:signal, String.t()}
signals_with_access_level =
Sanbase.Signal.access_map()
|> get_with_access_level(level)
|> Enum.map(&{:signal, &1})

# List of {:metric, String.t()}
metrics_with_access_level =
Sanbase.Metric.access_map()
|> get_with_access_level(level)
|> Enum.map(&{:metric, &1})

queries_with_access_level ++
signals_with_access_level ++
metrics_with_access_level
end

defp get_query_meta_field_list(field) do
Enum.map(@fields, fn f ->
{f, Map.get(@query_type.fields, f) |> Absinthe.Type.meta(field)}
Enum.map(query_type_fields(), fn f ->
{f, Map.get(query_type().fields, f) |> Absinthe.Type.meta(field)}
end)
end

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,39 +42,32 @@ defmodule Sanbase.Billing.Plan.StandardAccessChecker do

@doc documentation_ref: "# DOCS access-plans/index.md"

case ApiInfo.get_queries_without_access_level() do
[] ->
:ok
def check_queries_without_types() do
case ApiInfo.get_queries_without_access_level() do
[] ->
:ok

queries ->
require Sanbase.Break, as: Break
queries ->
require Sanbase.Break, as: Break

Break.break("""
There are GraphQL queries defined without specifying their access level.
The access level could be either `free` or `restricted`.
To define an access level, put `meta(access: <level>)` in the field definition.
Break.break("""
There are GraphQL queries defined without specifying their access level.
The access level could be either `free` or `restricted`.
To define an access level, put `meta(access: <level>)` in the field definition.

Queries without access level: #{inspect(queries)}
""")
Queries without access level: #{inspect(queries)}
""")
end
end

@free_query_or_argument ApiInfo.get_all_with_access_level(:free)
@free_query_or_argument_mapset MapSet.new(@free_query_or_argument)

@restricted_query_or_argument ApiInfo.get_all_with_access_level(:restricted)

@all_query_or_argument @free_query_or_argument ++ @restricted_query_or_argument

@min_plan_map ApiInfo.min_plan_map()

@doc ~s"""
Check if a query full access is given only to users with a plan higher than free.
A query can be restricted but still accessible by not-paid users or users with
lower plans. In this case historical and/or realtime data access can be cut off
"""
@spec restricted?(query_or_argument) :: boolean()
def restricted?(query_or_argument),
do: query_or_argument not in @free_query_or_argument_mapset
do: query_or_argument not in ApiInfo.get_all_with_access_level_mapset(:free)

@spec plan_has_access?(query_or_argument, requested_product, plan_name) :: boolean()
def plan_has_access?(query_or_argument, requested_product, plan_name) do
Expand All @@ -89,7 +82,7 @@ defmodule Sanbase.Billing.Plan.StandardAccessChecker do

@spec min_plan(product_code, query_or_argument) :: plan_name
def min_plan(product_code, query_or_argument) do
@min_plan_map[query_or_argument][product_code] || "FREE"
ApiInfo.min_plan_map()[query_or_argument][product_code] || "FREE"
end

@spec get_available_metrics_for_plan(plan_name, product_code, Atom.t()) ::
Expand All @@ -98,9 +91,9 @@ defmodule Sanbase.Billing.Plan.StandardAccessChecker do

def get_available_metrics_for_plan(plan_name, product_code, restriction_type) do
case restriction_type do
:free -> @free_query_or_argument
:restricted -> @restricted_query_or_argument
:all -> @all_query_or_argument
:free -> ApiInfo.get_all_with_access_level(:free)
:restricted -> ApiInfo.get_all_with_access_level(:restricted)
:all -> ApiInfo.get_all_with_any_access_level()
end
|> Stream.filter(&match?({:metric, _}, &1))
|> Stream.filter(&plan_has_access?(&1, product_code, plan_name))
Expand Down
48 changes: 36 additions & 12 deletions lib/sanbase_web/graphql/document/document_provider.ex
Original file line number Diff line number Diff line change
Expand Up @@ -27,19 +27,43 @@ defmodule SanbaseWeb.Graphql.DocumentProvider do
@doc false
@impl true
def pipeline(%Absinthe.Plug.Request.Query{pipeline: pipeline}) do
IO.inspect("RUNNING OWN DOCUMENT PROVIDER")

pipeline =
Enum.map(pipeline, fn
Absinthe.Phase.Schema.InlineFunctions ->
IO.puts("Overriding InlineFunctions")
{Absinthe.Phase.Schema.InlineFunctions, inline_always: true}

{Absinthe.Phase.Schema.Compile, options} ->
IO.puts("Overriding Absinthe.Phase.Schema.Compile")
{Absinthe.Phase.Schema.PopulatePersistentTerm, options}

{phase, options} ->
IO.inspect("Phase #{phase}")
{phase, options}

phase ->
IO.inspect("Phase #{phase}")
phase
end)

pipeline =
pipeline
|> Absinthe.Pipeline.insert_before(
Absinthe.Phase.Document.Complexity.Analysis,
SanbaseWeb.Graphql.Phase.Document.Complexity.Preprocess
)
|> Absinthe.Pipeline.insert_before(
Absinthe.Phase.Document.Execution.Resolution,
SanbaseWeb.Graphql.Phase.Document.Execution.CacheDocument
)
|> Absinthe.Pipeline.insert_after(
Absinthe.Phase.Document.Result,
SanbaseWeb.Graphql.Phase.Document.Execution.Idempotent
)

pipeline
|> Absinthe.Pipeline.insert_before(
Absinthe.Phase.Document.Complexity.Analysis,
SanbaseWeb.Graphql.Phase.Document.Complexity.Preprocess
)
|> Absinthe.Pipeline.insert_before(
Absinthe.Phase.Document.Execution.Resolution,
SanbaseWeb.Graphql.Phase.Document.Execution.CacheDocument
)
|> Absinthe.Pipeline.insert_after(
Absinthe.Phase.Document.Result,
SanbaseWeb.Graphql.Phase.Document.Execution.Idempotent
)
end

@doc false
Expand Down
1 change: 1 addition & 0 deletions lib/sanbase_web/graphql/schema.ex
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ defmodule SanbaseWeb.Graphql.Schema do
# Disable too many dependencies errors
# credo:disable-for-this-file
use Absinthe.Schema
@schema_provider Absinthe.Schema.PersistentTerm

alias SanbaseWeb.Graphql
alias SanbaseWeb.Graphql.{SanbaseRepo, SanbaseDataloader}
Expand Down
6 changes: 2 additions & 4 deletions lib/sanbase_web/router.ex
Original file line number Diff line number Diff line change
Expand Up @@ -99,8 +99,7 @@ defmodule SanbaseWeb.Router do
json_codec: Jason,
schema: SanbaseWeb.Graphql.Schema,
document_providers: [
SanbaseWeb.Graphql.DocumentProvider,
Absinthe.Plug.DocumentProvider.Default
SanbaseWeb.Graphql.DocumentProvider
],
analyze_complexity: true,
max_complexity: 50_000,
Expand All @@ -116,8 +115,7 @@ defmodule SanbaseWeb.Router do
schema: SanbaseWeb.Graphql.Schema,
socket: SanbaseWeb.UserSocket,
document_providers: [
SanbaseWeb.Graphql.DocumentProvider,
Absinthe.Plug.DocumentProvider.Default
SanbaseWeb.Graphql.DocumentProvider
],
analyze_complexity: true,
max_complexity: 50_000,
Expand Down