From b2d3baacf89d626f77a6810028d7470abb2b9734 Mon Sep 17 00:00:00 2001 From: Ben Wilson Date: Thu, 16 Nov 2017 07:32:41 -0500 Subject: [PATCH 1/2] ordered result example --- lib/blog_web/json.ex | 8 +++ lib/blog_web/result.ex | 112 ++++++++++++++++++++++++++++++++ lib/blog_web/router.ex | 11 +++- mix.exs | 1 + mix.lock | 1 + test/blog_web/plug_web_test.exs | 29 +++++++++ 6 files changed, 161 insertions(+), 1 deletion(-) create mode 100644 lib/blog_web/json.ex create mode 100644 lib/blog_web/result.ex create mode 100644 test/blog_web/plug_web_test.exs diff --git a/lib/blog_web/json.ex b/lib/blog_web/json.ex new file mode 100644 index 0000000..bdbbeed --- /dev/null +++ b/lib/blog_web/json.ex @@ -0,0 +1,8 @@ +defmodule BlogWeb.JSON do + def encode!(data, _) do + :jiffy.encode(data) + end + def decode(string) do + {:ok, :jiffy.decode(string, [:return_maps])} + end +end diff --git a/lib/blog_web/result.ex b/lib/blog_web/result.ex new file mode 100644 index 0000000..006fd68 --- /dev/null +++ b/lib/blog_web/result.ex @@ -0,0 +1,112 @@ +defmodule BlogWeb.OrdGraphQLResult do + + @moduledoc false + + # Produces data fit for external encoding from annotated value tree + + alias Absinthe.{Blueprint, Phase, Type} + use Absinthe.Phase + + @spec run(Blueprint.t | Phase.Error.t, Keyword.t) :: {:ok, map} + def run(%Blueprint{} = bp, _options \\ []) do + result = Map.merge(bp.result, process(bp)) + {:ok, %{bp | result: result}} + end + + defp process(blueprint) do + result = case blueprint.execution do + %{validation_errors: [], result: result} -> + {:ok, data(result, [])} + %{validation_errors: errors} -> + {:validation_failed, errors} + end + format_result(result) + end + + defp format_result(:execution_failed) do + %{data: nil} + end + defp format_result({:ok, {data, []}}) do + %{data: data} + end + defp format_result({:ok, {data, errors}}) do + errors = errors |> Enum.uniq |> Enum.map(&format_error/1) + %{data: data, errors: errors} + end + defp format_result({:validation_failed, errors}) do + errors = errors |> Enum.uniq |> Enum.map(&format_error/1) + %{errors: errors} + end + defp format_result({:parse_failed, error}) do + %{errors: [format_error(error)]} + end + + defp data(%{errors: [_|_] = field_errors}, errors), do: {nil, field_errors ++ errors} + + # Leaf + defp data(%{value: nil}, errors), do: {nil, errors} + defp data(%{value: value, emitter: emitter}, errors) do + value = + case Type.unwrap(emitter.schema_node.type) do + %Type.Scalar{} = schema_node -> + Type.Scalar.serialize(schema_node, value) + %Type.Enum{} = schema_node -> + Type.Enum.serialize(schema_node, value) + end + {value, errors} + end + + # Object + defp data(%{fields: fields}, errors), do: field_data(fields, errors) + + # List + defp data(%{values: values}, errors), do: list_data(values, errors) + + defp list_data(fields, errors, acc \\ []) + defp list_data([], errors, acc), do: {:lists.reverse(acc), errors} + defp list_data([%{errors: errs} = field | fields], errors, acc) do + {value, errors} = data(field, errors) + list_data(fields, errs ++ errors, [value | acc]) + end + + defp field_data(fields, errors, acc \\ []) + defp field_data([], errors, acc), do: {{:lists.reverse(acc)}, errors} + defp field_data([%Absinthe.Resolution{} = res | _], _errors, _acc) do + raise """ + Found unresolved resolution struct! + + You probably forgot to run the resolution phase again. + + #{inspect res} + """ + end + defp field_data([field | fields], errors, acc) do + {value, errors} = data(field, errors) + field_data(fields, errors, [{field_name(field.emitter), value} | acc]) + end + + defp field_name(%{alias: nil, name: name}), do: name + defp field_name(%{alias: name}), do: name + defp field_name(%{name: name}), do: name + + defp format_error(%Phase.Error{locations: []} = error) do + error_object = %{message: error.message} + Map.merge(error.extra, error_object) + end + defp format_error(%Phase.Error{} = error) do + error_object = %{ + message: error.message, + locations: Enum.flat_map(error.locations, &format_location/1), + } + error_object = case error.path do + [] -> error_object + path -> Map.put(error_object, :path, path) + end + Map.merge(Map.new(error.extra), error_object) + end + + defp format_location(%{line: line, column: col}) do + [%{line: line || 0, column: col || 0}] + end + defp format_location(_), do: [] +end diff --git a/lib/blog_web/router.ex b/lib/blog_web/router.ex index a152966..1e1e3c4 100644 --- a/lib/blog_web/router.ex +++ b/lib/blog_web/router.ex @@ -13,7 +13,16 @@ defmodule BlogWeb.Router do schema: BlogWeb.Schema forward "/", Absinthe.Plug, - schema: BlogWeb.Schema + schema: BlogWeb.Schema, + json_codec: BlogWeb.JSON, + pipeline: {__MODULE__, :absinthe_pipeline} + end + + def absinthe_pipeline(config, pipeline_opts) do + pipeline_opts = Keyword.put(pipeline_opts, :result_phase, BlogWeb.OrdGraphQLResult) + config.schema_mod + |> Absinthe.Pipeline.for_document(pipeline_opts) + |> Absinthe.Pipeline.replace(Absinthe.Phase.Document.Result, BlogWeb.OrdGraphQLResult) end end diff --git a/mix.exs b/mix.exs index f0b4ba1..fb68193 100644 --- a/mix.exs +++ b/mix.exs @@ -45,6 +45,7 @@ defmodule Blog.Mixfile do {:absinthe_ecto, ">= 0.0.0"}, {:comeonin, "~> 4.0"}, {:argon2_elixir, "~> 1.2"}, + {:jiffy, ">= 0.0.0"}, ] end diff --git a/mix.lock b/mix.lock index 89ace17..e396bd4 100644 --- a/mix.lock +++ b/mix.lock @@ -12,6 +12,7 @@ "ecto_enum": {:hex, :ecto_enum, "0.3.1", "28771a73c195553b32b434f926302092ba072ba2b50224b8d63081cad5e0846b", [], [{:ecto, ">= 0.13.1", [hex: :ecto, repo: "hexpm", optional: false]}, {:mariaex, ">= 0.3.0", [hex: :mariaex, repo: "hexpm", optional: true]}, {:postgrex, ">= 0.8.3", [hex: :postgrex, repo: "hexpm", optional: true]}], "hexpm"}, "elixir_make": {:hex, :elixir_make, "0.4.0", "992f38fabe705bb45821a728f20914c554b276838433349d4f2341f7a687cddf", [], [], "hexpm"}, "gettext": {:hex, :gettext, "0.13.1", "5e0daf4e7636d771c4c71ad5f3f53ba09a9ae5c250e1ab9c42ba9edccc476263", [], [], "hexpm"}, + "jiffy": {:hex, :jiffy, "0.14.11", "919a87d491c5a6b5e3bbc27fafedc3a0761ca0b4c405394f121f582fd4e3f0e5", [], [], "hexpm"}, "mime": {:hex, :mime, "1.1.0", "01c1d6f4083d8aa5c7b8c246ade95139620ef8effb009edde934e0ec3b28090a", [], [], "hexpm"}, "phoenix": {:hex, :phoenix, "1.3.0", "1c01124caa1b4a7af46f2050ff11b267baa3edb441b45dbf243e979cd4c5891b", [], [{:cowboy, "~> 1.0", [hex: :cowboy, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 1.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:plug, "~> 1.3.3 or ~> 1.4", [hex: :plug, repo: "hexpm", optional: false]}, {:poison, "~> 2.2 or ~> 3.0", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm"}, "phoenix_ecto": {:hex, :phoenix_ecto, "3.3.0", "702f6e164512853d29f9d20763493f2b3bcfcb44f118af2bc37bb95d0801b480", [], [{:ecto, "~> 2.1", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.9", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"}, diff --git a/test/blog_web/plug_web_test.exs b/test/blog_web/plug_web_test.exs new file mode 100644 index 0000000..7f43dbf --- /dev/null +++ b/test/blog_web/plug_web_test.exs @@ -0,0 +1,29 @@ +defmodule BlogWebTest do + use BlogWeb.ConnCase + + @query """ + { + posts { + id, title + } + } + """ + test "ordered result" do + conn = build_conn() + conn = get conn, "/api", query: @query + assert conn.resp_body == ~s({"data":{"posts":[{"id":"1","title":"Test Post"}]}}) + end + + @query """ + { + posts { + title, id + } + } + """ + test "ordered result works in reverse" do + conn = build_conn() + conn = get conn, "/api", query: @query + assert conn.resp_body == ~s({"data":{"posts":[{"title":"Test Post","id":"1"}]}}) + end +end From 99ef6af57b321602d0040038e8823f90f3d0dca2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Kav=C3=ADk?= Date: Sun, 19 Nov 2017 20:51:38 +0100 Subject: [PATCH 2/2] prepared for nanobox, graphiql ordered --- boxfile.yml | 14 ++++++++++++++ config/dev.exs | 8 ++++---- config/prod.exs | 2 +- config/test.exs | 6 +++--- lib/blog_web/json.ex | 2 +- lib/blog_web/router.ex | 4 +++- mix.lock | 2 +- 7 files changed, 27 insertions(+), 11 deletions(-) create mode 100644 boxfile.yml diff --git a/boxfile.yml b/boxfile.yml new file mode 100644 index 0000000..540c744 --- /dev/null +++ b/boxfile.yml @@ -0,0 +1,14 @@ +run.config: + engine: elixir + + engine.config: + runtime: elixir-1.5 + + engine.config: + erlang_runtime: erlang-20.1 + + dev_packages: + - inotify-tools + +data.db: + image: nanobox/postgresql:9.6 diff --git a/config/dev.exs b/config/dev.exs index cf75fed..845bfb2 100644 --- a/config/dev.exs +++ b/config/dev.exs @@ -7,7 +7,7 @@ use Mix.Config # watchers to your application. For example, we use it # with brunch.io to recompile .js and .css sources. config :blog, BlogWeb.Endpoint, - http: [port: 4000], + http: [port: 8080], debug_errors: true, code_reloader: true, check_origin: false, @@ -39,8 +39,8 @@ config :phoenix, :stacktrace_depth, 20 # Configure your database config :blog, Blog.Repo, adapter: Ecto.Adapters.Postgres, - username: "postgres", - password: "postgres", + username: System.get_env("DATA_DB_USER") || "postgres", + password: System.get_env("DATA_DB_PASS") || "postgres", + hostname: System.get_env("DATA_DB_HOST") || "localhost", database: "blog_dev", - hostname: "localhost", pool_size: 10 diff --git a/config/prod.exs b/config/prod.exs index 5db1e78..e070f59 100644 --- a/config/prod.exs +++ b/config/prod.exs @@ -15,7 +15,7 @@ use Mix.Config # which you typically run after static files are built. config :blog, BlogWeb.Endpoint, load_from_system_env: true, - url: [host: "example.com", port: 80], + url: [host: "example.com", port: 8080], cache_static_manifest: "priv/static/cache_manifest.json" # Do not print debug messages in production diff --git a/config/test.exs b/config/test.exs index 74c4ef2..1363f43 100644 --- a/config/test.exs +++ b/config/test.exs @@ -12,8 +12,8 @@ config :logger, level: :warn # Configure your database config :blog, Blog.Repo, adapter: Ecto.Adapters.Postgres, - username: "postgres", - password: "postgres", + username: System.get_env("DATA_DB_USER") || "postgres", + password: System.get_env("DATA_DB_PASS") || "postgres", + hostname: System.get_env("DATA_DB_HOST") || "localhost", database: "blog_test", - hostname: "localhost", pool: Ecto.Adapters.SQL.Sandbox diff --git a/lib/blog_web/json.ex b/lib/blog_web/json.ex index bdbbeed..57f339e 100644 --- a/lib/blog_web/json.ex +++ b/lib/blog_web/json.ex @@ -1,6 +1,6 @@ defmodule BlogWeb.JSON do def encode!(data, _) do - :jiffy.encode(data) + :jiffy.encode(data, [:use_nil]) end def decode(string) do {:ok, :jiffy.decode(string, [:return_maps])} diff --git a/lib/blog_web/router.ex b/lib/blog_web/router.ex index 1e1e3c4..adb1184 100644 --- a/lib/blog_web/router.ex +++ b/lib/blog_web/router.ex @@ -10,7 +10,9 @@ defmodule BlogWeb.Router do pipe_through :api forward "/graphiql", Absinthe.Plug.GraphiQL, - schema: BlogWeb.Schema + schema: BlogWeb.Schema, + json_codec: BlogWeb.JSON, + pipeline: {__MODULE__, :absinthe_pipeline} forward "/", Absinthe.Plug, schema: BlogWeb.Schema, diff --git a/mix.lock b/mix.lock index e396bd4..78381fc 100644 --- a/mix.lock +++ b/mix.lock @@ -12,7 +12,7 @@ "ecto_enum": {:hex, :ecto_enum, "0.3.1", "28771a73c195553b32b434f926302092ba072ba2b50224b8d63081cad5e0846b", [], [{:ecto, ">= 0.13.1", [hex: :ecto, repo: "hexpm", optional: false]}, {:mariaex, ">= 0.3.0", [hex: :mariaex, repo: "hexpm", optional: true]}, {:postgrex, ">= 0.8.3", [hex: :postgrex, repo: "hexpm", optional: true]}], "hexpm"}, "elixir_make": {:hex, :elixir_make, "0.4.0", "992f38fabe705bb45821a728f20914c554b276838433349d4f2341f7a687cddf", [], [], "hexpm"}, "gettext": {:hex, :gettext, "0.13.1", "5e0daf4e7636d771c4c71ad5f3f53ba09a9ae5c250e1ab9c42ba9edccc476263", [], [], "hexpm"}, - "jiffy": {:hex, :jiffy, "0.14.11", "919a87d491c5a6b5e3bbc27fafedc3a0761ca0b4c405394f121f582fd4e3f0e5", [], [], "hexpm"}, + "jiffy": {:hex, :jiffy, "0.14.11", "919a87d491c5a6b5e3bbc27fafedc3a0761ca0b4c405394f121f582fd4e3f0e5", [:rebar3], [], "hexpm"}, "mime": {:hex, :mime, "1.1.0", "01c1d6f4083d8aa5c7b8c246ade95139620ef8effb009edde934e0ec3b28090a", [], [], "hexpm"}, "phoenix": {:hex, :phoenix, "1.3.0", "1c01124caa1b4a7af46f2050ff11b267baa3edb441b45dbf243e979cd4c5891b", [], [{:cowboy, "~> 1.0", [hex: :cowboy, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 1.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:plug, "~> 1.3.3 or ~> 1.4", [hex: :plug, repo: "hexpm", optional: false]}, {:poison, "~> 2.2 or ~> 3.0", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm"}, "phoenix_ecto": {:hex, :phoenix_ecto, "3.3.0", "702f6e164512853d29f9d20763493f2b3bcfcb44f118af2bc37bb95d0801b480", [], [{:ecto, "~> 2.1", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.9", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"},