From fa9ca42c8ea4329233fd2005106d6cc1f7e0ed0d Mon Sep 17 00:00:00 2001 From: Sebastian Abondano Date: Wed, 24 Oct 2018 16:23:41 -0400 Subject: [PATCH] First GraphQL API commit Why: * We want to offer a GraphQL API with support for queries to get a block by number. * Issue link: n/a This change addresses the need by: * Adding absinthe, absinthe_phoenix, and absinthe_plug dependencies. * Editing router to support the GraphQL API at `/graphql` and GraphiQL at `graphiql`. * Creating `BlockScoutWeb.Schema` with query that allows users to get a block by number. * Creating `BlockScoutWeb.Resolvers.Block` with a single resolver function to get a block by number. * Creating `BlockScoutWeb.Schema.Types` with a `block` object type. --- .../lib/block_scout_web/resolvers/block.ex | 12 ++ .../lib/block_scout_web/router.ex | 7 ++ .../lib/block_scout_web/schema.ex | 18 +++ .../lib/block_scout_web/schema/types.ex | 24 ++++ apps/block_scout_web/mix.exs | 6 + .../schema/query/block_test.exs | 106 ++++++++++++++++++ mix.lock | 3 + 7 files changed, 176 insertions(+) create mode 100644 apps/block_scout_web/lib/block_scout_web/resolvers/block.ex create mode 100644 apps/block_scout_web/lib/block_scout_web/schema.ex create mode 100644 apps/block_scout_web/lib/block_scout_web/schema/types.ex create mode 100644 apps/block_scout_web/test/block_scout_web/schema/query/block_test.exs diff --git a/apps/block_scout_web/lib/block_scout_web/resolvers/block.ex b/apps/block_scout_web/lib/block_scout_web/resolvers/block.ex new file mode 100644 index 0000000000..a970f7c4ef --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/resolvers/block.ex @@ -0,0 +1,12 @@ +defmodule BlockScoutWeb.Resolvers.Block do + @moduledoc false + + alias Explorer.Chain + + def get_by(_, %{number: number}, _) do + case Chain.number_to_block(number) do + {:ok, _} = result -> result + {:error, :not_found} -> {:error, "Block number #{number} was not found."} + end + end +end diff --git a/apps/block_scout_web/lib/block_scout_web/router.ex b/apps/block_scout_web/lib/block_scout_web/router.ex index 16c68ef413..70d8906044 100644 --- a/apps/block_scout_web/lib/block_scout_web/router.ex +++ b/apps/block_scout_web/lib/block_scout_web/router.ex @@ -38,6 +38,13 @@ defmodule BlockScoutWeb.Router do }) end + forward("/graphql", Absinthe.Plug, schema: BlockScoutWeb.Schema) + + forward("/graphiql", Absinthe.Plug.GraphiQL, + schema: BlockScoutWeb.Schema, + interface: :playground + ) + scope "/", BlockScoutWeb do pipe_through(:browser) diff --git a/apps/block_scout_web/lib/block_scout_web/schema.ex b/apps/block_scout_web/lib/block_scout_web/schema.ex new file mode 100644 index 0000000000..db30d8296b --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/schema.ex @@ -0,0 +1,18 @@ +defmodule BlockScoutWeb.Schema do + @moduledoc false + + use Absinthe.Schema + + alias BlockScoutWeb.Resolvers.Block + + import_types(Absinthe.Type.Custom) + import_types(BlockScoutWeb.Schema.Types) + + query do + @desc "Gets a block by number." + field :block, :block do + arg(:number, non_null(:integer)) + resolve(&Block.get_by/3) + end + end +end diff --git a/apps/block_scout_web/lib/block_scout_web/schema/types.ex b/apps/block_scout_web/lib/block_scout_web/schema/types.ex new file mode 100644 index 0000000000..a191f2d6c6 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/schema/types.ex @@ -0,0 +1,24 @@ +defmodule BlockScoutWeb.Schema.Types do + @moduledoc false + + use Absinthe.Schema.Notation + + @desc """ + A package of data that contains zero or more transactions, the hash of the previous block ("parent"), and optionally + other data. Because each block (except for the initial "genesis block") points to the previous block, the data + structure that they form is called a "blockchain". + """ + object :block do + field(:consensus, :boolean) + field(:difficulty, :decimal) + field(:gas_limit, :decimal) + field(:gas_used, :decimal) + field(:nonce, :string) + field(:number, :integer) + field(:size, :integer) + field(:timestamp, :datetime) + field(:total_difficulty, :decimal) + field(:miner_hash, :string) + field(:parent_hash, :string) + end +end diff --git a/apps/block_scout_web/mix.exs b/apps/block_scout_web/mix.exs index 71e04f5664..516fc836fc 100644 --- a/apps/block_scout_web/mix.exs +++ b/apps/block_scout_web/mix.exs @@ -62,6 +62,12 @@ defmodule BlockScoutWeb.Mixfile do # Type `mix help deps` for examples and options. defp deps do [ + # GraphQL toolkit + {:absinthe, "~> 1.4"}, + # Integrates Absinthe subscriptions with Phoenix + {:absinthe_phoenix, "~> 1.4"}, + # Plug support for Absinthe + {:absinthe_plug, "~> 1.4"}, {:bypass, "~> 0.8", only: :test}, {:cowboy, "~> 1.0"}, {:credo, "0.9.2", only: [:dev, :test], runtime: false}, diff --git a/apps/block_scout_web/test/block_scout_web/schema/query/block_test.exs b/apps/block_scout_web/test/block_scout_web/schema/query/block_test.exs new file mode 100644 index 0000000000..571c2cbc2d --- /dev/null +++ b/apps/block_scout_web/test/block_scout_web/schema/query/block_test.exs @@ -0,0 +1,106 @@ +defmodule BlockScoutWeb.Schema.Query.BlockTest do + use BlockScoutWeb.ConnCase + + describe "block field" do + test "with valid argument 'number', returns all expected fields", %{conn: conn} do + block = insert(:block) + + query = """ + query ($number: Int!) { + block(number: $number) { + consensus + difficulty + gas_limit + gas_used + nonce + number + size + timestamp + total_difficulty + miner_hash + parent_hash + parent_hash + } + } + """ + + variables = %{"number" => block.number} + + conn = get(conn, "/graphql", query: query, variables: variables) + + assert json_response(conn, 200) == %{ + "data" => %{ + "block" => %{ + "consensus" => block.consensus, + "difficulty" => to_string(block.difficulty), + "gas_limit" => to_string(block.gas_limit), + "gas_used" => to_string(block.gas_used), + "nonce" => to_string(block.nonce), + "number" => block.number, + "size" => block.size, + "timestamp" => DateTime.to_iso8601(block.timestamp), + "total_difficulty" => to_string(block.total_difficulty), + "miner_hash" => to_string(block.miner_hash), + "parent_hash" => to_string(block.parent_hash) + } + } + } + end + + test "errors for non-existent block number", %{conn: conn} do + block = insert(:block) + non_existent_block_number = block.number + 1 + + query = """ + query ($number: Int!) { + block(number: $number) { + number + } + } + """ + + variables = %{"number" => non_existent_block_number} + + conn = get(conn, "/graphql", query: query, variables: variables) + + assert %{"errors" => [error]} = json_response(conn, 200) + assert error["message"] =~ ~s(Block number #{non_existent_block_number} was not found) + end + + test "errors if argument 'number' is missing", %{conn: conn} do + insert(:block) + + query = """ + { + block { + number + } + } + """ + + conn = get(conn, "/graphql", query: query) + + assert %{"errors" => [error]} = json_response(conn, 400) + assert error["message"] == ~s(In argument "number": Expected type "Int!", found null.) + end + + test "errors if argument 'number' is not an integer", %{conn: conn} do + insert(:block) + + query = """ + query ($number: Int!) { + block(number: $number) { + number + } + } + """ + + variables = %{"number" => "invalid"} + + conn = get(conn, "/graphql", query: query, variables: variables) + + assert %{"errors" => [error]} = json_response(conn, 400) + assert error["message"] =~ ~s(Argument "number" has invalid value) + end + end +end diff --git a/mix.lock b/mix.lock index ee9ea9c2fd..417c16acb9 100644 --- a/mix.lock +++ b/mix.lock @@ -1,6 +1,9 @@ %{ "abi": {:hex, :abi, "0.1.12", "87ae04cb09e2308db7b3c350584dc3934de0e308f6a056ba82be5756b081a1ca", [:mix], [{:exth_crypto, "~> 0.1.4", [hex: :exth_crypto, repo: "hexpm", optional: false]}], "hexpm"}, "abnf2": {:hex, :abnf2, "0.1.2", "6f8792b8ac3288dba5fc889c2bceae9fe78f74e1a7b36bea9726ffaa9d7bef95", [:mix], []}, + "absinthe": {:hex, :absinthe, "1.4.13", "81eb2ff41f1b62cd6e992955f62c22c042d1079b7936c27f5f7c2c806b8fc436", [:mix], [{:dataloader, "~> 1.0.0", [hex: :dataloader, repo: "hexpm", optional: true]}, {:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm"}, + "absinthe_phoenix": {:hex, :absinthe_phoenix, "1.4.3", "cea34e7ebbc9a252038c1f1164878ee86bcb108905fe462be77efacda15c1e70", [:mix], [{:absinthe, "~> 1.4.0", [hex: :absinthe, repo: "hexpm", optional: false]}, {:absinthe_plug, "~> 1.4.0", [hex: :absinthe_plug, repo: "hexpm", optional: false]}, {:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.2", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.10.5 or ~> 2.11", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 1.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:poison, "~> 2.0 or ~> 3.0", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm"}, + "absinthe_plug": {:hex, :absinthe_plug, "1.4.5", "f63d52a76c870cd5f11d4bed8f61351ab5c5f572c5eb0479a0137f9f730ba33d", [:mix], [{:absinthe, "~> 1.4.11", [hex: :absinthe, repo: "hexpm", optional: false]}, {:plug, "~> 1.3.2 or ~> 1.4", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"}, "accept": {:hex, :accept, "0.3.3", "548ebb6fb2e8b0d170e75bb6123aea6ceecb0189bb1231eeadf52eac08384a97", [:rebar3], [], "hexpm"}, "bcrypt_elixir": {:hex, :bcrypt_elixir, "1.0.6", "58a865939b3106d5ad4841f660955b958be6db955dda034fbbc1069dbacb97fa", [:make, :mix], [{:elixir_make, "~> 0.4", [hex: :elixir_make, optional: false]}]}, "benchee": {:hex, :benchee, "0.13.1", "bd93ca05be78bcb6159c7176230efeda2f724f7ffd485515175ca411dff4893e", [:mix], [{:deep_merge, "~> 0.1", [hex: :deep_merge, optional: false]}]},