Repository: absinthe-graphql/absinthe_plug (elixir)

Issue:
Dynamically generated max complexity
Hello! 👋 First of all, thank you for Absinthe and `Absinthe.Plug`!

I'm wondering if the maintainers would be open to the addition of a feature to dynamically determine the max complexity per query. I'd like to be able to say something like, "User X made a query, they have Y complexity allowance left, will I allow them to make this query?"

Of course this doesn't match the primary use case. 😄 

Whereas the current API looks like this:

```elixir
plug Absinthe.Plug,
  schema: MyAppWeb.Schema,
  analyze_complexity: true,
  max_complexity: 50
```

I would like to propose expanding the API to support this as well:

```elixir
plug Absinthe.Plug,
  schema: MyAppWeb.Schema,
  analyze_complexity: true,
  max_complexity: &MyModule.max_complexity/1
```

Where `&MyModule.max_complexity/1` is a function that is given the context and query and returns back an integer.

Thank you for considering! I would be glad to whip up a PR if you're open to this. Cheers!

Pull request:
Set all plug options via put_options
This PR wires up the ability to set complexity settings via `put_options`

@benwilson512 These make sense to set on a per-request basis, but I'm not sure if the other options do...

closes #241 

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 473db59..39adbc1 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,9 @@
 # Changelog
 
+## v1.5.4
+
+- Feature: [Set all plug options via put_options](https://github.com/absinthe-graphql/absinthe_plug/pull/243)
+
 ## v1.5.3
 
 - Chore: [Loosen Absinthe version](https://github.com/absinthe-graphql/absinthe_plug/pull/242)
diff --git a/lib/absinthe/plug.ex b/lib/absinthe/plug.ex
index 681f7a0..aa2ebaf 100644
--- a/lib/absinthe/plug.ex
+++ b/lib/absinthe/plug.ex
@@ -124,7 +124,27 @@ defmodule Absinthe.Plug do
 
   alias __MODULE__.Request
 
-  @raw_options [:analyze_complexity, :max_complexity]
+  @init_options [
+    :adapter,
+    :context,
+    :no_query_message,
+    :json_codec,
+    :pipeline,
+    :document_providers,
+    :schema,
+    :serializer,
+    :content_type,
+    :before_send,
+    :log_level,
+    :pubsub,
+    :analyze_complexity,
+    :max_complexity,
+    :transport_batch_payload_key
+  ]
+  @raw_options [
+    :analyze_complexity,
+    :max_complexity
+  ]
 
   @type function_name :: atom
 
@@ -140,6 +160,7 @@ defmodule Absinthe.Plug do
   - `:content_type` -- (Optional) The content type of the response. Should probably be set if `:serializer` option is used. Defaults to `"application/json"`.
   - `:before_send` -- (Optional) Set a value(s) on the connection after resolution but before values are sent to the client.
   - `:log_level` -- (Optional) Set the logger level for Absinthe Logger. Defaults to `:debug`.
+  - `:pubsub` -- (Optional) Pub Sub module for Subscriptions.
   - `:analyze_complexity` -- (Optional) Set whether to calculate the complexity of incoming GraphQL queries.
   - `:max_complexity` -- (Optional) Set the maximum allowed complexity of the GraphQL query. If a document’s calculated complexity exceeds the maximum, resolution will be skipped and an error will be returned in the result detailing the calculated and maximum complexities.
   - `:transport_batch_payload_key` -- (Optional) Set whether or not to nest Transport Batch request results in a `payload` key. Older clients expected this key to be present, but newer clients have dropped this pattern. (default: `true`)
@@ -162,6 +183,7 @@ defmodule Absinthe.Plug do
           content_type: String.t(),
           before_send: {module, atom},
           log_level: Logger.level(),
+          pubsub: module | nil,
           transport_batch_payload_key: boolean
         ]
 
@@ -299,6 +321,13 @@ defmodule Absinthe.Plug do
   end
 
   defp update_config(conn, config) do
+    config
+    |> update_config(:pubsub, conn)
+    |> update_config(:raw_options, conn)
+    |> update_config(:init_options, conn)
+  end
+
+  defp update_config(config, :pubsub, conn) do
     pubsub = config[:pubsub] || config.context[:pubsub] || conn.private[:phoenix_endpoint]
 
     if pubsub do
@@ -308,6 +337,19 @@ defmodule Absinthe.Plug do
     end
   end
 
+  defp update_config(config, :raw_options, %{private: %{absinthe: absinthe}}) do
+    raw_options = Map.take(absinthe, @raw_options) |> Map.to_list()
+    update_in(config.raw_options, &Keyword.merge(&1, raw_options))
+  end
+
+  defp update_config(config, :init_options, %{private: %{absinthe: absinthe}}) do
+    Map.merge(config, Map.take(absinthe, @init_options -- @raw_options))
+  end
+
+  defp update_config(config, _, _conn) do
+    config
+  end
+
   def subscribe(conn, topic, %{context: %{pubsub: pubsub}} = config) do
     pubsub.subscribe(topic)
 


Test patch:
diff --git a/test/lib/absinthe/plug_test.exs b/test/lib/absinthe/plug_test.exs
index 15ee447..7a5364e 100644
--- a/test/lib/absinthe/plug_test.exs
+++ b/test/lib/absinthe/plug_test.exs
@@ -512,6 +512,53 @@ defmodule Absinthe.PlugTest do
       assert conn.private.absinthe.context.current_user.id == 1
       assert conn.private.absinthe.root_value.field_on_root_value == "foo"
     end
+
+    test "sets complexity" do
+      opts = Absinthe.Plug.init(schema: TestSchema)
+
+      query = "{expensive}"
+
+      assert %{status: 200, resp_body: resp_body} =
+               conn(:post, "/", Jason.encode!(%{"query" => query}))
+               |> Absinthe.Plug.put_options(
+                 analyze_complexity: true,
+                 max_complexity: 100
+               )
+               |> put_req_header("content-type", "application/json")
+               |> call(opts)
+
+      expected = %{
+        "errors" => [
+          %{
+            "locations" => [%{"column" => 2, "line" => 1}],
+            "message" => "Field expensive is too complex: complexity is 1000 and maximum is 100"
+          },
+          %{
+            "locations" => [%{"column" => 1, "line" => 1}],
+            "message" => "Operation is too complex: complexity is 1000 and maximum is 100"
+          }
+        ]
+      }
+
+      assert expected == resp_body
+    end
+
+    test "sets all init options" do
+      opts = Absinthe.Plug.init(schema: TestSchema)
+      query = ""
+
+      assert %{resp_body: resp_body, resp_headers: resp_headers} =
+               conn(:post, "/", Jason.encode!(%{"query" => query}))
+               |> Absinthe.Plug.put_options(
+                 no_query_message: "No query!!",
+                 content_type: "text/who-knows"
+               )
+               |> put_req_header("content-type", "application/json")
+               |> call(opts)
+
+      assert %{"errors" => [%{"message" => "No query!!"}]} = resp_body
+      assert {"content-type", "text/who-knows; charset=utf-8"} in resp_headers
+    end
   end
 
   describe "assign_context/2" do