diff --git a/README.md b/README.md index 7b9b4c5..ef77625 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,4 @@ + # Absinthe Tutorial Code This repository houses the example code for the official Absinthe tutorial @@ -24,3 +25,6 @@ should: ## License See [LICENSE.md](./LICENSE.md). + +For an actual local tutorial guide, see TUTORIAL.txt + diff --git a/TUTORIAL.txt b/TUTORIAL.txt new file mode 100644 index 0000000..d13c375 --- /dev/null +++ b/TUTORIAL.txt @@ -0,0 +1,91 @@ +Welcome and congratualtions! + +Since you found this textfile, you have got off and landed on your feet! + +This short tutorial goes through a few steps just to introduce the basic concepts. + +First things first - the structure, location, and prerequisite. + +The structure you just checked out looks like this (don't panic! :-) ): + +. +└── absinthe_tutorial <-- we are here - this is the location of the tutorial root directory! + ├── LICENSE.md + ├── README.md + ├── config + │ ├── config.exs + │ ├── dev.exs + │ ├── prod.exs + │ └── test.exs + ├── lib + │ ├── blog + │ │ ├── accounts + │ │ │ ├── accounts.ex + │ │ │ ├── contact.ex + │ │ │ └── user.ex + │ │ ├── application.ex + │ │ ├── content + │ │ │ ├── content.ex + │ │ │ └── post.ex + │ │ └── repo.ex + │ ├── blog.ex + │ ├── blog_web + │ │ ├── channels + │ │ │ └── user_socket.ex + │ │ ├── context.ex + │ │ ├── endpoint.ex + │ │ ├── gettext.ex + │ │ ├── resolvers + │ │ │ ├── accounts.ex + │ │ │ └── content.ex + │ │ ├── router.ex + │ │ ├── schema + │ │ │ ├── account_types.ex + │ │ │ └── content_types.ex + │ │ ├── schema.ex + │ │ └── views + │ │ ├── error_helpers.ex + │ │ └── error_view.ex + │ └── blog_web.ex + ├── mix.exs + ├── mix.lock + ├── priv + │ ├── gettext + │ │ ├── en + │ │ │ └── LC_MESSAGES + │ │ │ └── errors.po + │ │ └── errors.pot + │ └── repo + │ ├── migrations + │ │ ├── 20171024194851_create_users.exs + │ │ ├── 20171024194852_create_posts.exs + │ │ └── 20171024211706_create_contacts.exs + │ └── seeds.exs + └── test + ├── blog_web + │ └── views + │ └── error_view_test.exs + ├── support + │ ├── channel_case.ex + │ ├── conn_case.ex + │ └── data_case.ex + └── test_helper.exs + +In order to be able to run this tutorial, you should already have the following installed: +Elixir - see https://elixir-lang.org or https://github.com/absinthe-graphql/absinthe +Absinthe - see https://github.com/absinthe-graphql/absinthe + +---- + +From the tutorial root directory directory, you should now run the following commands to get you going: + +#1 + mix deps.get + +followed by + +#2 + mix ecto.setup + + +Enjoy! 8=) 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 new file mode 100644 index 0000000..57f339e --- /dev/null +++ b/lib/blog_web/json.ex @@ -0,0 +1,8 @@ +defmodule BlogWeb.JSON do + def encode!(data, _) do + :jiffy.encode(data, [:use_nil]) + 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..adb1184 100644 --- a/lib/blog_web/router.ex +++ b/lib/blog_web/router.ex @@ -10,10 +10,21 @@ 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 + 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..78381fc 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", [: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"}, 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 diff --git a/tutorial/complex-arguments.html b/tutorial/complex-arguments.html new file mode 100644 index 0000000..296fc67 --- /dev/null +++ b/tutorial/complex-arguments.html @@ -0,0 +1,83 @@ +
In preparation for supporting comments on our blog, let's create users. We're building a modern mobile first blog of course, and thus want to support either a phone number or an email as the contact method for a user.
+We want to support the following mutations.
+Support creation of a user with their email address:
+mutation CreateEmailUser {
+ createUser(contact: {type: EMAIL, value: "foo@bar.com"}, name: "Jane", password: "hunter1") {
+ id
+ contacts {
+ type
+ value
+ }
+ }
+}
+And by using their phone number:
+mutation CreatePhoneUser {
+ createUser(contact: {type: PHONE, value: "+1 123 5551212"}, name: "Joe", password: "hunter2") {
+ id
+ contacts {
+ type
+ value
+ }
+ }
+}
+To do this we need the ability to create nested arguments. GraphQL has input objects for this purpose. Input objects, like regular object, contain key value pairs, but they are intended for input only (you can't do circular references with them for example).
+Another notion we'll look at here is an enumerable type. We only want to support contact types "email" and "phone" at the moment, and GraphQL gives us the ability to specify this in our schema.
Let's start with our :contact_type Enum. In blog_web/schema/account_types.ex:
enum :contact_type do
+ value :phone, as: "phone"
+ value :email, as: "email"
+end
+We're using the :as option here to make sure the parsed enum is represented by a string when it's passed to our controllers; this is to ease integration with our Ecto schema (by default, the enum values are passed as atoms).
++The standard convention for representing incoming enum values in GraphQL documents are in all caps. For instance, given our settings here, the accepted values would be
+PHONEandWhile the
+enummacro supports configuring this incoming format, we highly recommend you just use the GraphQL convention.
Now if a user tries to send some other kind of contact type they'll get a nice error without any extra effort on your part. Enum types are not a substitute for modeling layer validations however, be sure to still enforce things like this on that layer too.
+Now for our contact input object.
+In blog_web/schema/account_types.ex:
input_object :contact_input do
+ field :type, non_null(:contact_type)
+ field :value, non_null(:string)
+end
+Note that we name this type :contact_input. Input object types have their own names, and the _input suffix is common.
++Important: It's very important to remember that only input types---basically scalars and input objects---can be used to model input.
+
Finally our schema, in blog_web/schema.ex:
mutation do
+
+ #... other mutations
+
+ @desc "Create a user"
+ field :create_user, :user do
+ arg :name, non_null(:string)
+ arg :contact, non_null(:contact_input)
+ arg :password, non_null(:string)
+
+ resolve &Resolvers.Accounts.create_user/3
+ end
+
+end
+Suppose in our database that we store contact information in a different database table. Our mutation would be used to create both records in this case.
+There does not need to be a one to one correspondence between how data is structured in your underlying data store and how things are presented by your GraphQL API.
+Our resolver, blog_web/resolvers/accounts.ex might look something like this:
def create_user(_parent, args, %{context: %{current_user: %{admin: true}}}) do
+ Blog.Accounts.create_user(args)
+end
+def create_user(_parent, args, _resolution) do
+ {:error, "Access denied"}
+end
+You'll notice we're checking for :current_user again in our Absinthe context, just as we did before for posts. In this case we're taking the authorization check a step further and verifying that only administrators (in this simple example, an administrator is a user account with :admin set to true) can create a user.
Everyone else gets an "Access denied" error for this field.
++To see the Ecto-related implementation of the
+Blog.Accounts.create_user/1function and the (stubbed) authentication logic we're using for this example, see the absinthe_tutorial repository.
Here's our mutation in action in GraphiQL.
+
++Note we're sending a
+Authorizationheader to authenticate, which a plug is handling. Make sure to read the related guide for more information on how to set-up authentication in your own applications.Our simple tutorial application is just using a simple stub: any authorization token logs you in the first user. Obviously not what you want in production!
+
Now let's wrap things up.
diff --git a/tutorial/complex-arguments.md.html b/tutorial/complex-arguments.md.html new file mode 100644 index 0000000..296fc67 --- /dev/null +++ b/tutorial/complex-arguments.md.html @@ -0,0 +1,83 @@ +In preparation for supporting comments on our blog, let's create users. We're building a modern mobile first blog of course, and thus want to support either a phone number or an email as the contact method for a user.
+We want to support the following mutations.
+Support creation of a user with their email address:
+mutation CreateEmailUser {
+ createUser(contact: {type: EMAIL, value: "foo@bar.com"}, name: "Jane", password: "hunter1") {
+ id
+ contacts {
+ type
+ value
+ }
+ }
+}
+And by using their phone number:
+mutation CreatePhoneUser {
+ createUser(contact: {type: PHONE, value: "+1 123 5551212"}, name: "Joe", password: "hunter2") {
+ id
+ contacts {
+ type
+ value
+ }
+ }
+}
+To do this we need the ability to create nested arguments. GraphQL has input objects for this purpose. Input objects, like regular object, contain key value pairs, but they are intended for input only (you can't do circular references with them for example).
+Another notion we'll look at here is an enumerable type. We only want to support contact types "email" and "phone" at the moment, and GraphQL gives us the ability to specify this in our schema.
Let's start with our :contact_type Enum. In blog_web/schema/account_types.ex:
enum :contact_type do
+ value :phone, as: "phone"
+ value :email, as: "email"
+end
+We're using the :as option here to make sure the parsed enum is represented by a string when it's passed to our controllers; this is to ease integration with our Ecto schema (by default, the enum values are passed as atoms).
++The standard convention for representing incoming enum values in GraphQL documents are in all caps. For instance, given our settings here, the accepted values would be
+PHONEandWhile the
+enummacro supports configuring this incoming format, we highly recommend you just use the GraphQL convention.
Now if a user tries to send some other kind of contact type they'll get a nice error without any extra effort on your part. Enum types are not a substitute for modeling layer validations however, be sure to still enforce things like this on that layer too.
+Now for our contact input object.
+In blog_web/schema/account_types.ex:
input_object :contact_input do
+ field :type, non_null(:contact_type)
+ field :value, non_null(:string)
+end
+Note that we name this type :contact_input. Input object types have their own names, and the _input suffix is common.
++Important: It's very important to remember that only input types---basically scalars and input objects---can be used to model input.
+
Finally our schema, in blog_web/schema.ex:
mutation do
+
+ #... other mutations
+
+ @desc "Create a user"
+ field :create_user, :user do
+ arg :name, non_null(:string)
+ arg :contact, non_null(:contact_input)
+ arg :password, non_null(:string)
+
+ resolve &Resolvers.Accounts.create_user/3
+ end
+
+end
+Suppose in our database that we store contact information in a different database table. Our mutation would be used to create both records in this case.
+There does not need to be a one to one correspondence between how data is structured in your underlying data store and how things are presented by your GraphQL API.
+Our resolver, blog_web/resolvers/accounts.ex might look something like this:
def create_user(_parent, args, %{context: %{current_user: %{admin: true}}}) do
+ Blog.Accounts.create_user(args)
+end
+def create_user(_parent, args, _resolution) do
+ {:error, "Access denied"}
+end
+You'll notice we're checking for :current_user again in our Absinthe context, just as we did before for posts. In this case we're taking the authorization check a step further and verifying that only administrators (in this simple example, an administrator is a user account with :admin set to true) can create a user.
Everyone else gets an "Access denied" error for this field.
++To see the Ecto-related implementation of the
+Blog.Accounts.create_user/1function and the (stubbed) authentication logic we're using for this example, see the absinthe_tutorial repository.
Here's our mutation in action in GraphiQL.
+
++Note we're sending a
+Authorizationheader to authenticate, which a plug is handling. Make sure to read the related guide for more information on how to set-up authentication in your own applications.Our simple tutorial application is just using a simple stub: any authorization token logs you in the first user. Obviously not what you want in production!
+
Now let's wrap things up.
diff --git a/tutorial/conclusion.html b/tutorial/conclusion.html new file mode 100644 index 0000000..1de7659 --- /dev/null +++ b/tutorial/conclusion.html @@ -0,0 +1,11 @@ +With this we have a basic GraphQL based API for a blog. Head on over to the github page if you want the final code.
+We hope to expand this tutorial to include a comment system that will acquaint you with Union types and Fragments in the coming days.
+Head on over to the topic guides for further reading, and see the community page for information on how to get help, ask questions, or contribute!
+This tutorial is a work in progress, and while it covers the basics of using Absinthe, there is plenty more that can be added and improved upon. It's important that it's kept up-to-date, too, so if you notice something that's slipped by us, please help us fix it!
+Please contribute your GitHub issues (and pull requests!):
+guides/tutorial in the absinthe repository. It's in Markdown and easy to edit!With this we have a basic GraphQL based API for a blog. Head on over to the github page if you want the final code.
+We hope to expand this tutorial to include a comment system that will acquaint you with Union types and Fragments in the coming days.
+Head on over to the topic guides for further reading, and see the community page for information on how to get help, ask questions, or contribute!
+This tutorial is a work in progress, and while it covers the basics of using Absinthe, there is plenty more that can be added and improved upon. It's important that it's kept up-to-date, too, so if you notice something that's slipped by us, please help us fix it!
+Please contribute your GitHub issues (and pull requests!):
+guides/tutorial in the absinthe repository. It's in Markdown and easy to edit!Maybe you like good performance, or you realized that you are filling your objects with fields that need resolvers like
+@desc "A user of the blog"
+ object :user do
+ field :id, :id
+ field :name, :string
+ field :contacts, list_of(:contact)
+ field :posts, list_of(:post) do
+ arg :date, :date
+ resolve &Resolvers.Content.list_posts/3
+ end
+ end
+This is going to get tedious and error-prone very quickly what if we could support a query that supports associations like
+@desc "A user of the blog"
+ object :user do
+ field :id, :id
+ field :name, :string
+ field :contacts, list_of(:contact)
+ field :posts, list_of(:post) do
+ arg :date, :date
+ resolve: dataloader(Content))
+ end
+ end
+This way associations are all handled in the context business logic aware conditions, to support this is actually surprisingly simple.
+Since we had already setup users to load associated posts we can change that to use dataloader to illustrate how much simpler this gets.
+Let's start by adding dataloader as a dependency in mix.exs:
defp deps do
+ [
+ {:dataloader, "~> 1.0.4"}
+ << other deps >>
+ ]
+Next, we need to set up dataloader in our context which allows us to load associations using rules:
+In lib/blog/content.ex:
def data(), do: Dataloader.Ecto.new(Repo, query: &query/2)
+
+ def query(queryable, params) do
+
+ queryable
+ end
+This sets up a loader that can use pattern matching to load different rules for different queryables, also note this function is passed in the context as the second parameter and that can be used for further filtering.
+Then let's add a configuration to our schema (in lib/blog_web/schema.ex) so that we can allow Absinthe to use Dataloader:
defmodule BlogWeb.Schema do
+ use Absinthe.Schema
+
+ def context(ctx) do
+ loader =
+ Dataloader.new()
+ |> Dataloader.add_source(Content, Content.data())
+
+ Map.put(ctx, :loader, loader)
+ end
+
+ def plugins do
+ [Absinthe.Middleware.Dataloader | Absinthe.Plugin.defaults()]
+ end
+
+ # << rest of the file>>
+The loader is all set up, now let's modify the resolver to use Dataloader. In lib/blog_web/schema/account_types.ex modify the user object to look as follows:
@desc "A user of the blog"
+ object :user do
+ field :id, :id
+ field :name, :string
+ field :contacts, list_of(:contact)
+ field :posts, list_of(:post) do
+ arg :date, :date
+ resolve: dataloader(Content))
+ end
+ end
+That's it! You are now loading associations using Dataloader
+While the above examples are simple and straightforward we can use other strategies with loading associations consider the following:
+object :user do
+ field :posts, list_of(:post), resolve: fn user, args, %{context: %{loader: loader}} ->
+ loader
+ |> Dataloader.load(Blog, :posts, user)
+ |> on_load(fn loader ->
+ {:ok, Dataloader.get(loader, Blog, :posts, user)}
+ end)
+ end
+In this example, we are passing some args go the query in the context where our source lives. For example, this function now receives args as params meaning we can do now do fun stuff like apply rules to our queries like the following:
def query(query, %{has_admin_rights: true}), do: query
+
+def query(query, _), do: from(a in query, select_merge: %{street_number: nil})
+This example is from the awesome EmCasa Application :) you can see how the author is only loading street numbers if a user has admin rights and the same used in a resolver.
+Check out the docs for more non-trivial ways of using Dataloader.
diff --git a/tutorial/dataloader.md.html b/tutorial/dataloader.md.html new file mode 100644 index 0000000..7bed511 --- /dev/null +++ b/tutorial/dataloader.md.html @@ -0,0 +1,85 @@ +Maybe you like good performance, or you realized that you are filling your objects with fields that need resolvers like
+@desc "A user of the blog"
+ object :user do
+ field :id, :id
+ field :name, :string
+ field :contacts, list_of(:contact)
+ field :posts, list_of(:post) do
+ arg :date, :date
+ resolve &Resolvers.Content.list_posts/3
+ end
+ end
+This is going to get tedious and error-prone very quickly what if we could support a query that supports associations like
+@desc "A user of the blog"
+ object :user do
+ field :id, :id
+ field :name, :string
+ field :contacts, list_of(:contact)
+ field :posts, list_of(:post) do
+ arg :date, :date
+ resolve: dataloader(Content))
+ end
+ end
+This way associations are all handled in the context business logic aware conditions, to support this is actually surprisingly simple.
+Since we had already setup users to load associated posts we can change that to use dataloader to illustrate how much simpler this gets.
+Let's start by adding dataloader as a dependency in mix.exs:
defp deps do
+ [
+ {:dataloader, "~> 1.0.4"}
+ << other deps >>
+ ]
+Next, we need to set up dataloader in our context which allows us to load associations using rules:
+In lib/blog/content.ex:
def data(), do: Dataloader.Ecto.new(Repo, query: &query/2)
+
+ def query(queryable, params) do
+
+ queryable
+ end
+This sets up a loader that can use pattern matching to load different rules for different queryables, also note this function is passed in the context as the second parameter and that can be used for further filtering.
+Then let's add a configuration to our schema (in lib/blog_web/schema.ex) so that we can allow Absinthe to use Dataloader:
defmodule BlogWeb.Schema do
+ use Absinthe.Schema
+
+ def context(ctx) do
+ loader =
+ Dataloader.new()
+ |> Dataloader.add_source(Content, Content.data())
+
+ Map.put(ctx, :loader, loader)
+ end
+
+ def plugins do
+ [Absinthe.Middleware.Dataloader | Absinthe.Plugin.defaults()]
+ end
+
+ # << rest of the file>>
+The loader is all set up, now let's modify the resolver to use Dataloader. In lib/blog_web/schema/account_types.ex modify the user object to look as follows:
@desc "A user of the blog"
+ object :user do
+ field :id, :id
+ field :name, :string
+ field :contacts, list_of(:contact)
+ field :posts, list_of(:post) do
+ arg :date, :date
+ resolve: dataloader(Content))
+ end
+ end
+That's it! You are now loading associations using Dataloader
+While the above examples are simple and straightforward we can use other strategies with loading associations consider the following:
+object :user do
+ field :posts, list_of(:post), resolve: fn user, args, %{context: %{loader: loader}} ->
+ loader
+ |> Dataloader.load(Blog, :posts, user)
+ |> on_load(fn loader ->
+ {:ok, Dataloader.get(loader, Blog, :posts, user)}
+ end)
+ end
+In this example, we are passing some args go the query in the context where our source lives. For example, this function now receives args as params meaning we can do now do fun stuff like apply rules to our queries like the following:
def query(query, %{has_admin_rights: true}), do: query
+
+def query(query, _), do: from(a in query, select_merge: %{street_number: nil})
+This example is from the awesome EmCasa Application :) you can see how the author is only loading street numbers if a user has admin rights and the same used in a resolver.
+Check out the docs for more non-trivial ways of using Dataloader.
diff --git a/tutorial/mutations.html b/tutorial/mutations.html new file mode 100644 index 0000000..1063fae --- /dev/null +++ b/tutorial/mutations.html @@ -0,0 +1,41 @@ +A blog is no good without new content. We want to support a mutation to create a blog post:
+mutation CreatePost {
+ createPost(title: "Second", body: "We're off to a great start!") {
+ id
+ }
+}
+Now we just need to define a mutation portion of our schema and a :create_post field:
In blog_web/schema.ex:
mutation do
+
+ @desc "Create a post"
+ field :create_post, type: :post do
+ arg :title, non_null(:string)
+ arg :body, non_null(:string)
+ arg :published_at, :naive_datetime
+
+ resolve &Resolvers.Content.create_post/3
+ end
+
+end
+The resolver in this case is responsible for making any changes and returning an {:ok, post} tuple matching the :post type we defined earlier:
In our blog_web/resolvers/content.ex module, we'll add the create_post/3 resolver function:
def create_post(_parent, args, %{context: %{current_user: user}}) do
+ Blog.Content.create_post(user, args)
+end
+def create_post(_parent, _args, _resolution) do
+ {:error, "Access denied"}
+end
+++Obviously things can go wrong in a mutation. To learn more about the types of error results that Absinthe supports, read the guide.
+
This resolver adds a new concept: authorization. The resolution struct (that is, an Absinthe.Resolution) passed to the resolver as the third argument carries along with it the Absinthe context, a data structure that serves as the integration point with external mechanisms---like a Plug that authenticates the current user. You can learn more about how the context can be used in the Context and Authentication guide.
Going back to the resolver code:
+Blog.Content.create_post/2 function is invoked. It will return a tuple suitable for return. (To read the Ecto-related nitty gritty, check out the absinthe_tutorial repository.)Now let's take a look at more complex arguments.
diff --git a/tutorial/mutations.md.html b/tutorial/mutations.md.html new file mode 100644 index 0000000..1063fae --- /dev/null +++ b/tutorial/mutations.md.html @@ -0,0 +1,41 @@ +A blog is no good without new content. We want to support a mutation to create a blog post:
+mutation CreatePost {
+ createPost(title: "Second", body: "We're off to a great start!") {
+ id
+ }
+}
+Now we just need to define a mutation portion of our schema and a :create_post field:
In blog_web/schema.ex:
mutation do
+
+ @desc "Create a post"
+ field :create_post, type: :post do
+ arg :title, non_null(:string)
+ arg :body, non_null(:string)
+ arg :published_at, :naive_datetime
+
+ resolve &Resolvers.Content.create_post/3
+ end
+
+end
+The resolver in this case is responsible for making any changes and returning an {:ok, post} tuple matching the :post type we defined earlier:
In our blog_web/resolvers/content.ex module, we'll add the create_post/3 resolver function:
def create_post(_parent, args, %{context: %{current_user: user}}) do
+ Blog.Content.create_post(user, args)
+end
+def create_post(_parent, _args, _resolution) do
+ {:error, "Access denied"}
+end
+++Obviously things can go wrong in a mutation. To learn more about the types of error results that Absinthe supports, read the guide.
+
This resolver adds a new concept: authorization. The resolution struct (that is, an Absinthe.Resolution) passed to the resolver as the third argument carries along with it the Absinthe context, a data structure that serves as the integration point with external mechanisms---like a Plug that authenticates the current user. You can learn more about how the context can be used in the Context and Authentication guide.
Going back to the resolver code:
+Blog.Content.create_post/2 function is invoked. It will return a tuple suitable for return. (To read the Ecto-related nitty gritty, check out the absinthe_tutorial repository.)Now let's take a look at more complex arguments.
diff --git a/tutorial/our-first-query.html b/tutorial/our-first-query.html new file mode 100644 index 0000000..9a81f5a --- /dev/null +++ b/tutorial/our-first-query.html @@ -0,0 +1,94 @@ +The first thing our viewers want is a list of our blog posts, so that's what we're going to give them. Here's the query we want to support:
+{
+ posts {
+ title
+ body
+ }
+}
+To do this we're going to need a schema. Let's create some basic types for our schema, starting with a :post. GraphQL has several fundamental types on top of which all of our types will be built. The Object type is the right one to use when representing a set of key value pairs.
Since our Post Ecto schema lives in the Blog.Content Phoenix context, we'll define its GraphQL counterpart type, :post, in a matching BlogWeb.Schema.ContentTypes module:
In blog_web/schema/content_types.ex:
defmodule BlogWeb.Schema.ContentTypes do
+ use Absinthe.Schema.Notation
+
+ object :post do
+ field :id, :id
+ field :title, :string
+ field :body, :string
+ end
+end
+++The GraphQL specification requires that type names be unique, TitleCased words. Absinthe does this automatically for us, extrapolating from our type identifier (in this case
+:postgives us"Post". If really needed, we could provide a custom type name as a:nameoption to theobjectmacro.
If you're curious what the type :id is used by the :id field, see the GraphQL spec. It's an opaque value, and in our case is just the regular Ecto id, but serialized as a string.
With our type completed we can now write a basic schema that will let us query a set of posts.
+In blog_web/schema.ex:
defmodule BlogWeb.Schema do
+ use Absinthe.Schema
+ import_types BlogWeb.Schema.ContentTypes
+
+ alias BlogWeb.Resolvers
+
+ query do
+
+ @desc "Get all posts"
+ field :posts, list_of(:post) do
+ resolve &Resolvers.Content.list_posts/3
+ end
+
+ end
+
+end
+++For more information on the macros available to build a schema, see their definitions in Absinthe.Schema and Absinthe.Schema.Notation.
+
This uses a resolver module we've created (again, to match the Phoenix context naming) at blog_web/resolvers/content.ex:
defmodule BlogWeb.Resolvers.Content do
+
+ def list_posts(_parent, _args, _resolution) do
+ {:ok, Blog.Content.list_posts()}
+ end
+
+end
+Queries are defined as fields inside the GraphQL object returned by our query function. We created a posts query that has a type list_of(:post) and is resolved by our BlogWeb.Resolvers.Content.list_posts/3 function. Later we'll talk more about the resolver function parameters; for now just remember that resolver functions can take two forms:
The job of the resolver function is to return the data for the requested field. Our resolver calls out to the Blog.Content module, which is where all the domain logic for posts lives, invoking its list_posts/0 function, then returns the posts in an :ok tuple.
++Resolvers can return a wide variety of results, to include errors and configuration for advanced plugins that further process the data.
+If you're asking yourself what the implementation of the domain logic looks like, and exactly how the related Ecto schemas are built, read through the code in the absinthe_tutorial repository. The tutorial content here is intentionally focused on the Absinthe-specific code.
+
Now that we have the functional pieces in place, let's configure our Phoenix router to wire this into HTTP:
+In blog_web/router.ex:
defmodule BlogWeb.Router do
+ use BlogWeb, :router
+
+ pipeline :api do
+ plug :accepts, ["json"]
+ end
+
+ scope "/api" do
+ pipe_through :api
+
+ forward "/graphiql", Absinthe.Plug.GraphiQL,
+ schema: BlogWeb.Schema
+
+ forward "/", Absinthe.Plug,
+ schema: BlogWeb.Schema
+
+ end
+
+end
+In addition to our API, we've wired in a handy GraphiQL user interface to play with it. Absinthe integrates both the classic GraphiQL and more advanced GraphiQL Workspace interfaces as part of the absinthe_plug package.
+Now let's check to make sure everything is working. Start the server:
+$ mix phx.server
+Absinthe does a number of sanity checks during compilation, so if you misspell a type or make another schema-related gaffe, you'll be notified.
+Once it's up-and-running, take a look at http://localhost:4000/api/graphiql:
+
Make sure that the URL is pointing to the correct place and press the play button. If everything goes according to plan, you should see something like this:

Now let's look at how we can add arguments to our queries.
diff --git a/tutorial/our-first-query.md.html b/tutorial/our-first-query.md.html new file mode 100644 index 0000000..9a81f5a --- /dev/null +++ b/tutorial/our-first-query.md.html @@ -0,0 +1,94 @@ +The first thing our viewers want is a list of our blog posts, so that's what we're going to give them. Here's the query we want to support:
+{
+ posts {
+ title
+ body
+ }
+}
+To do this we're going to need a schema. Let's create some basic types for our schema, starting with a :post. GraphQL has several fundamental types on top of which all of our types will be built. The Object type is the right one to use when representing a set of key value pairs.
Since our Post Ecto schema lives in the Blog.Content Phoenix context, we'll define its GraphQL counterpart type, :post, in a matching BlogWeb.Schema.ContentTypes module:
In blog_web/schema/content_types.ex:
defmodule BlogWeb.Schema.ContentTypes do
+ use Absinthe.Schema.Notation
+
+ object :post do
+ field :id, :id
+ field :title, :string
+ field :body, :string
+ end
+end
+++The GraphQL specification requires that type names be unique, TitleCased words. Absinthe does this automatically for us, extrapolating from our type identifier (in this case
+:postgives us"Post". If really needed, we could provide a custom type name as a:nameoption to theobjectmacro.
If you're curious what the type :id is used by the :id field, see the GraphQL spec. It's an opaque value, and in our case is just the regular Ecto id, but serialized as a string.
With our type completed we can now write a basic schema that will let us query a set of posts.
+In blog_web/schema.ex:
defmodule BlogWeb.Schema do
+ use Absinthe.Schema
+ import_types BlogWeb.Schema.ContentTypes
+
+ alias BlogWeb.Resolvers
+
+ query do
+
+ @desc "Get all posts"
+ field :posts, list_of(:post) do
+ resolve &Resolvers.Content.list_posts/3
+ end
+
+ end
+
+end
+++For more information on the macros available to build a schema, see their definitions in Absinthe.Schema and Absinthe.Schema.Notation.
+
This uses a resolver module we've created (again, to match the Phoenix context naming) at blog_web/resolvers/content.ex:
defmodule BlogWeb.Resolvers.Content do
+
+ def list_posts(_parent, _args, _resolution) do
+ {:ok, Blog.Content.list_posts()}
+ end
+
+end
+Queries are defined as fields inside the GraphQL object returned by our query function. We created a posts query that has a type list_of(:post) and is resolved by our BlogWeb.Resolvers.Content.list_posts/3 function. Later we'll talk more about the resolver function parameters; for now just remember that resolver functions can take two forms:
The job of the resolver function is to return the data for the requested field. Our resolver calls out to the Blog.Content module, which is where all the domain logic for posts lives, invoking its list_posts/0 function, then returns the posts in an :ok tuple.
++Resolvers can return a wide variety of results, to include errors and configuration for advanced plugins that further process the data.
+If you're asking yourself what the implementation of the domain logic looks like, and exactly how the related Ecto schemas are built, read through the code in the absinthe_tutorial repository. The tutorial content here is intentionally focused on the Absinthe-specific code.
+
Now that we have the functional pieces in place, let's configure our Phoenix router to wire this into HTTP:
+In blog_web/router.ex:
defmodule BlogWeb.Router do
+ use BlogWeb, :router
+
+ pipeline :api do
+ plug :accepts, ["json"]
+ end
+
+ scope "/api" do
+ pipe_through :api
+
+ forward "/graphiql", Absinthe.Plug.GraphiQL,
+ schema: BlogWeb.Schema
+
+ forward "/", Absinthe.Plug,
+ schema: BlogWeb.Schema
+
+ end
+
+end
+In addition to our API, we've wired in a handy GraphiQL user interface to play with it. Absinthe integrates both the classic GraphiQL and more advanced GraphiQL Workspace interfaces as part of the absinthe_plug package.
+Now let's check to make sure everything is working. Start the server:
+$ mix phx.server
+Absinthe does a number of sanity checks during compilation, so if you misspell a type or make another schema-related gaffe, you'll be notified.
+Once it's up-and-running, take a look at http://localhost:4000/api/graphiql:
+
Make sure that the URL is pointing to the correct place and press the play button. If everything goes according to plan, you should see something like this:

Now let's look at how we can add arguments to our queries.
diff --git a/tutorial/query-arguments.html b/tutorial/query-arguments.html new file mode 100644 index 0000000..849300a --- /dev/null +++ b/tutorial/query-arguments.html @@ -0,0 +1,159 @@ +Our GraphQL API would be pretty boring (and useless) if clients couldn't retrieve filtered data.
+Let's assume that our API needs to add the ability to look-up users by their ID and get the posts that they've authored. Here's what a basic query to do that might look like:
+{
+ user(id: "1") {
+ name
+ posts {
+ id
+ title
+ }
+ }
+}
+The query includes a field argument, id, contained within the parentheses after the user field name. To make this all work, we need to modify our schema a bit.
First, let's create a :user type and define its relationship to :post while we're at it. We'll create a new module for the account-related types and put it there; in blog_web/schema/account_types.ex:
defmodule BlogWeb.Schema.AccountTypes do
+ use Absinthe.Schema.Notation
+
+ @desc "A user of the blog"
+ object :user do
+ field :id, :id
+ field :name, :string
+ field :email, :string
+ field :posts, list_of(:post)
+ end
+
+end
+The :posts field points to a list of :post results. (This matches up with what we have on the Ecto side, where Blog.Accounts.User defines a has_many association with Blog.Content.Post.)
We've already defined the :post type, but let's go ahead and add an :author field that points back to our :user type. In blog_web/schema/content_types.ex:
object :post do
+
+ # post fields we defined earlier...
+
+ field :author, :user
+
+end
+Now let's add the :user field to our query root object in our schema, defining a mandatory :id argument and using the Resolvers.Accounts.find_user/3 resolver function. We also need to make sure we import the types from BlogWeb.Schema.AccountTypes so that :user is available.
In blog_web/schema.ex:
defmodule BlogWeb.Schema do
+ use Absinthe.Schema
+
+ import_types Absinthe.Type.Custom
+
+ # Add this `import_types`:
+ import_types BlogWeb.Schema.AccountTypes
+
+ import_types BlogWeb.Schema.ContentTypes
+
+ alias BlogWeb.Resolvers
+
+ query do
+
+ @desc "Get all posts"
+ field :posts, list_of(:post) do
+ resolve &Resolvers.Content.list_posts/3
+ end
+
+ # Add this field:
+ @desc "Get a user of the blog"
+ field :user, :user do
+ arg :id, non_null(:id)
+ resolve &Resolvers.Accounts.find_user/3
+ end
+
+ end
+
+end
+Now lets use the argument in our resolver. In blog_web/resolvers/accounts.ex:
defmodule BlogWeb.Resolvers.Accounts do
+
+ def find_user(_parent, %{id: id}, _resolution) do
+ case Blog.Accounts.find_user(id) do
+ nil ->
+ {:error, "User ID #{id} not found"}
+ user ->
+ {:ok, user}
+ end
+ end
+
+end
+Our schema marks the :id argument as non_null, so we can be certain we will receive it. If :id is left out of the query, Absinthe will return an informative error to the user, and the resolve function will not be called.
++If you have experience writing Phoenix controller actions, you might wonder why we can match incoming arguments with atoms instead of having to use strings.
+The answer is simple: you've defined the arguments in the schema using atom identifiers, so Absinthe knows what arguments will be used ahead of time, and will coerce as appropriate---culling any extraneous arguments given to a query. This means that all arguments can be supplied to the resolve functions with atom keys.
+
Finally you'll see that we can handle the possibility that the query, while valid from GraphQL's perspective, may still ask for a user that does not exist. We've decided to return an error in that case.
+++There's a valid argument for just returning
+{:ok, nil}when a record can't be found. Whether the absence of data constitutes an error is a decision you get to make.
Let's assume we want to query all posts from a user published within a given time range. First, let's add a new field to our :post object type, :published_at.
The GraphQL specification doesn't define any official date or time types, but it does support custom scalar types (you can read more about them in the related guide, and Absinthe ships with several built-in scalar types. We'll use :naive_datetime (which doesn't include timezone information) here.
Edit blog_web/schema/content_types.ex:
defmodule BlogWeb.Schema.ContentTypes do
+ use Absinthe.Schema.Notation
+
+ @desc "A blog post"
+ object :post do
+ field :id, :id
+ field :title, :string
+ field :body, :string
+ field :author, :user
+ # Add this:
+ field :published_at, :naive_datetime
+ end
+end
+To make the :naive_datetime type available, add an import_types line to your blog_web/schema.ex:
import_types Absinthe.Type.Custom
+++For more information about how types are imported, read the guide on the topic.
+For now, just remember that
+import_typesshould only be used in top-level schema module. (Think of it like a manifest.)
Here's the query we'd like to be able to use, getting the posts for a user on a given date:
+{
+ user(id: "1") {
+ name
+ posts(date: "2017-01-01") {
+ title
+ body
+ publishedAt
+ }
+ }
+}
+To use the passed date, we need to update our :user object type and make some changes to its :posts field; it needs to support a :date argument and use a custom resolver. In blog_web/schema/account_types.ex:
defmodule BlogWeb.Schema.AccountTypes do
+ use Absinthe.Schema.Notation
+
+ alias BlogWeb.Resolvers
+
+ object :user do
+ field :id, :id
+ field :name, :string
+ field :email, :string
+ # Add the block here:
+ field :posts, list_of(:post) do
+ arg :date, :date
+ resolve &Resolvers.Content.list_posts/3
+ end
+ end
+
+end
+For the resolver, we've added another function head to Resolvers.Content.list_posts/3. This illustrates how you can use the first argument to a resolver to match the parent object of a field. In this case, that parent object would be a Blog.Accounts.User Ecto schema:
# Add this:
+def list_posts(%Blog.Accounts.User{} = author, args, _resolution) do
+ {:ok, Blog.Content.list_posts(author, args)}
+end
+# Before this:
+def list_posts(_parent, _args, _resolution) do
+ {:ok, Blog.Content.list_posts()}
+end
+Here we pass on the user and arguments to the domain logic function, Blog.Content.list_posts/3, which will find the posts for the user and date (if it's provided; the :date argument is optional). The resolver, just as when it's used for the top level query :posts, returns the posts in an :ok tuple.
++Check out the full implementation of logic for
+Blog.Content.list_posts/3--and some simple seed data--in the absinthe_tutorial repository.
If you've done everything correctly (and have some data handy), if you start up your server with mix phx.server and head over to http://localhost:4000/api/graphiql, you should be able to play with the query.
It should look something like this:
+
Next up, we look at how to modify our data using mutations.
diff --git a/tutorial/query-arguments.md.html b/tutorial/query-arguments.md.html new file mode 100644 index 0000000..849300a --- /dev/null +++ b/tutorial/query-arguments.md.html @@ -0,0 +1,159 @@ +Our GraphQL API would be pretty boring (and useless) if clients couldn't retrieve filtered data.
+Let's assume that our API needs to add the ability to look-up users by their ID and get the posts that they've authored. Here's what a basic query to do that might look like:
+{
+ user(id: "1") {
+ name
+ posts {
+ id
+ title
+ }
+ }
+}
+The query includes a field argument, id, contained within the parentheses after the user field name. To make this all work, we need to modify our schema a bit.
First, let's create a :user type and define its relationship to :post while we're at it. We'll create a new module for the account-related types and put it there; in blog_web/schema/account_types.ex:
defmodule BlogWeb.Schema.AccountTypes do
+ use Absinthe.Schema.Notation
+
+ @desc "A user of the blog"
+ object :user do
+ field :id, :id
+ field :name, :string
+ field :email, :string
+ field :posts, list_of(:post)
+ end
+
+end
+The :posts field points to a list of :post results. (This matches up with what we have on the Ecto side, where Blog.Accounts.User defines a has_many association with Blog.Content.Post.)
We've already defined the :post type, but let's go ahead and add an :author field that points back to our :user type. In blog_web/schema/content_types.ex:
object :post do
+
+ # post fields we defined earlier...
+
+ field :author, :user
+
+end
+Now let's add the :user field to our query root object in our schema, defining a mandatory :id argument and using the Resolvers.Accounts.find_user/3 resolver function. We also need to make sure we import the types from BlogWeb.Schema.AccountTypes so that :user is available.
In blog_web/schema.ex:
defmodule BlogWeb.Schema do
+ use Absinthe.Schema
+
+ import_types Absinthe.Type.Custom
+
+ # Add this `import_types`:
+ import_types BlogWeb.Schema.AccountTypes
+
+ import_types BlogWeb.Schema.ContentTypes
+
+ alias BlogWeb.Resolvers
+
+ query do
+
+ @desc "Get all posts"
+ field :posts, list_of(:post) do
+ resolve &Resolvers.Content.list_posts/3
+ end
+
+ # Add this field:
+ @desc "Get a user of the blog"
+ field :user, :user do
+ arg :id, non_null(:id)
+ resolve &Resolvers.Accounts.find_user/3
+ end
+
+ end
+
+end
+Now lets use the argument in our resolver. In blog_web/resolvers/accounts.ex:
defmodule BlogWeb.Resolvers.Accounts do
+
+ def find_user(_parent, %{id: id}, _resolution) do
+ case Blog.Accounts.find_user(id) do
+ nil ->
+ {:error, "User ID #{id} not found"}
+ user ->
+ {:ok, user}
+ end
+ end
+
+end
+Our schema marks the :id argument as non_null, so we can be certain we will receive it. If :id is left out of the query, Absinthe will return an informative error to the user, and the resolve function will not be called.
++If you have experience writing Phoenix controller actions, you might wonder why we can match incoming arguments with atoms instead of having to use strings.
+The answer is simple: you've defined the arguments in the schema using atom identifiers, so Absinthe knows what arguments will be used ahead of time, and will coerce as appropriate---culling any extraneous arguments given to a query. This means that all arguments can be supplied to the resolve functions with atom keys.
+
Finally you'll see that we can handle the possibility that the query, while valid from GraphQL's perspective, may still ask for a user that does not exist. We've decided to return an error in that case.
+++There's a valid argument for just returning
+{:ok, nil}when a record can't be found. Whether the absence of data constitutes an error is a decision you get to make.
Let's assume we want to query all posts from a user published within a given time range. First, let's add a new field to our :post object type, :published_at.
The GraphQL specification doesn't define any official date or time types, but it does support custom scalar types (you can read more about them in the related guide, and Absinthe ships with several built-in scalar types. We'll use :naive_datetime (which doesn't include timezone information) here.
Edit blog_web/schema/content_types.ex:
defmodule BlogWeb.Schema.ContentTypes do
+ use Absinthe.Schema.Notation
+
+ @desc "A blog post"
+ object :post do
+ field :id, :id
+ field :title, :string
+ field :body, :string
+ field :author, :user
+ # Add this:
+ field :published_at, :naive_datetime
+ end
+end
+To make the :naive_datetime type available, add an import_types line to your blog_web/schema.ex:
import_types Absinthe.Type.Custom
+++For more information about how types are imported, read the guide on the topic.
+For now, just remember that
+import_typesshould only be used in top-level schema module. (Think of it like a manifest.)
Here's the query we'd like to be able to use, getting the posts for a user on a given date:
+{
+ user(id: "1") {
+ name
+ posts(date: "2017-01-01") {
+ title
+ body
+ publishedAt
+ }
+ }
+}
+To use the passed date, we need to update our :user object type and make some changes to its :posts field; it needs to support a :date argument and use a custom resolver. In blog_web/schema/account_types.ex:
defmodule BlogWeb.Schema.AccountTypes do
+ use Absinthe.Schema.Notation
+
+ alias BlogWeb.Resolvers
+
+ object :user do
+ field :id, :id
+ field :name, :string
+ field :email, :string
+ # Add the block here:
+ field :posts, list_of(:post) do
+ arg :date, :date
+ resolve &Resolvers.Content.list_posts/3
+ end
+ end
+
+end
+For the resolver, we've added another function head to Resolvers.Content.list_posts/3. This illustrates how you can use the first argument to a resolver to match the parent object of a field. In this case, that parent object would be a Blog.Accounts.User Ecto schema:
# Add this:
+def list_posts(%Blog.Accounts.User{} = author, args, _resolution) do
+ {:ok, Blog.Content.list_posts(author, args)}
+end
+# Before this:
+def list_posts(_parent, _args, _resolution) do
+ {:ok, Blog.Content.list_posts()}
+end
+Here we pass on the user and arguments to the domain logic function, Blog.Content.list_posts/3, which will find the posts for the user and date (if it's provided; the :date argument is optional). The resolver, just as when it's used for the top level query :posts, returns the posts in an :ok tuple.
++Check out the full implementation of logic for
+Blog.Content.list_posts/3--and some simple seed data--in the absinthe_tutorial repository.
If you've done everything correctly (and have some data handy), if you start up your server with mix phx.server and head over to http://localhost:4000/api/graphiql, you should be able to play with the query.
It should look something like this:
+
Next up, we look at how to modify our data using mutations.
diff --git a/tutorial/src/complex-arguments.md b/tutorial/src/complex-arguments.md new file mode 100644 index 0000000..0c6ff23 --- /dev/null +++ b/tutorial/src/complex-arguments.md @@ -0,0 +1,154 @@ +# Complex Arguments + +In preparation for supporting comments on our blog, let's create users. We're building +a modern mobile first blog of course, and thus want to support either a phone number +or an email as the contact method for a user. + +We want to support the following mutations. + +Support creation of a user with their email address: + +```graphql +mutation CreateEmailUser { + createUser(contact: {type: EMAIL, value: "foo@bar.com"}, name: "Jane", password: "hunter1") { + id + contacts { + type + value + } + } +} +``` + +And by using their phone number: + +```graphql +mutation CreatePhoneUser { + createUser(contact: {type: PHONE, value: "+1 123 5551212"}, name: "Joe", password: "hunter2") { + id + contacts { + type + value + } + } +} +``` + +To do this we need the ability to create nested arguments. GraphQL has input objects +for this purpose. Input objects, like regular object, contain key value pairs, but +they are intended for input only (you can't do circular references with them for example). + +Another notion we'll look at here is an enumerable type. We only want to support contact +types `"email"` and `"phone"` at the moment, and GraphQL gives us the ability to +specify this in our schema. + +Let's start with our `:contact_type` Enum. In `blog_web/schema/account_types.ex`: + +```graphql +enum :contact_type do + value :phone, as: "phone" + value :email, as: "email" +end +``` + +We're using the `:as` option here to make sure the parsed enum is represented by a string +when it's passed to our controllers; this is to ease integration with our Ecto schema +(by default, the enum values are passed as atoms). + +> The standard convention for representing incoming enum values in +> GraphQL documents are in all caps. For instance, given our settings +> here, the accepted values would be `PHONE` and `EMAIL` (without +> quotes). See the GraphQL document examples above for examples. +> +> While the `enum` macro supports configuring this incoming format, we +> highly recommend you just use the GraphQL convention. + +Now if a user tries to send some other kind of contact type they'll +get a nice error without any extra effort on your part. Enum types are +not a substitute for modeling layer validations however, be sure to +still enforce things like this on that layer too. + +Now for our contact input object. + +In `blog_web/schema/account_types.ex`: + +```graphql +input_object :contact_input do + field :type, non_null(:contact_type) + field :value, non_null(:string) +end +``` + +Note that we name this type `:contact_input`. Input object types have +their own names, and the `_input` suffix is common. + +> Important: It's very important to remember that only input +> types---basically scalars and input objects---can be used to model +> input. + +Finally our schema, in `blog_web/schema.ex`: + +```elixir +mutation do + + #... other mutations + + @desc "Create a user" + field :create_user, :user do + arg :name, non_null(:string) + arg :contact, non_null(:contact_input) + arg :password, non_null(:string) + + resolve &Resolvers.Accounts.create_user/3 + end + +end +``` + +Suppose in our database that we store contact information in a different database +table. Our mutation would be used to create both records in this case. + +There does not need to be a one to one correspondence between how data is structured +in your underlying data store and how things are presented by your GraphQL API. + +Our resolver, `blog_web/resolvers/accounts.ex` might look something like this: + +```elixir +def create_user(_parent, args, %{context: %{current_user: %{admin: true}}}) do + Blog.Accounts.create_user(args) +end +def create_user(_parent, args, _resolution) do + {:error, "Access denied"} +end +``` + +You'll notice we're checking for `:current_user` again in our Absinthe +context, just as we did before for posts. In this case we're taking +the authorization check a step further and verifying that only +administrators (in this simple example, an administrator is a user +account with `:admin` set to `true`) can create a user. + +Everyone else gets an `"Access denied"` error for this field. + +> To see the Ecto-related implementation of the +> `Blog.Accounts.create_user/1` function and the (stubbed) authentication logic we're +> using for this example, see the [absinthe_tutorial](https://github.com/absinthe-graphql/absinthe_tutorial) +> repository. + +Here's our mutation in action in GraphiQL. + +
+
+> Note we're sending a `Authorization` header to authenticate, which a
+> plug is handling. Make sure to read the
+> related [guide](context-and-authentication.html) for more
+> information on how to set-up authentication in your own
+> applications.
+>
+> Our simple tutorial application is just using a simple stub: any
+> authorization token logs you in the first user. Obviously not what
+> you want in production!
+
+## Next Step
+
+Now let's [wrap things up](conclusion.html).
diff --git a/tutorial/src/conclusion.md b/tutorial/src/conclusion.md
new file mode 100644
index 0000000..6e9ad6d
--- /dev/null
+++ b/tutorial/src/conclusion.md
@@ -0,0 +1,25 @@
+# Conclusion
+
+With this we have a basic GraphQL based API for a blog. Head on over
+to [the github page](https://github.com/absinthe-graphql/absinthe_tutorial) if
+you want the final code.
+
+We hope to expand this tutorial to include a comment system that will
+acquaint you with Union types and Fragments in the coming days.
+
+Head on over to the topic guides for further reading, and see
+the [community page](community.html) for information
+on how to get help, ask questions, or contribute!
+
+## Please Help!
+
+This tutorial is a work in progress, and while it covers the basics of
+using Absinthe, there is plenty more that can be added and improved
+upon. It's important that it's kept up-to-date, too, so if you notice
+something that's slipped by us, please help us fix it!
+
+Please contribute your GitHub issues (and pull requests!):
+
+- The tutorial text is under `guides/tutorial` in the [absinthe](https://github.com/absinthe-graphql/absinthe)
+ repository. It's in Markdown and easy to edit!
+- The tutorial code located in the [absinthe_tutorial](https://github.com/absinthe-graphql/absinthe_tutorial) repository.
diff --git a/tutorial/src/convert-md-to-html.sh b/tutorial/src/convert-md-to-html.sh
new file mode 100644
index 0000000..dfec878
--- /dev/null
+++ b/tutorial/src/convert-md-to-html.sh
@@ -0,0 +1 @@
+for i in *.md; do pandoc -f markdown -t html -o ../`basename -s .md $i`.html $i ; done
diff --git a/tutorial/src/dataloader.md b/tutorial/src/dataloader.md
new file mode 100644
index 0000000..8b3434a
--- /dev/null
+++ b/tutorial/src/dataloader.md
@@ -0,0 +1,124 @@
+# Dataloader
+
+Maybe you like good performance, or you realized that you are filling your objects with fields that need resolvers like
+
+```elixir
+@desc "A user of the blog"
+ object :user do
+ field :id, :id
+ field :name, :string
+ field :contacts, list_of(:contact)
+ field :posts, list_of(:post) do
+ arg :date, :date
+ resolve &Resolvers.Content.list_posts/3
+ end
+ end
+```
+
+This is going to get tedious and error-prone very quickly what if we could support a query that supports associations like
+
+```elixir
+@desc "A user of the blog"
+ object :user do
+ field :id, :id
+ field :name, :string
+ field :contacts, list_of(:contact)
+ field :posts, list_of(:post) do
+ arg :date, :date
+ resolve: dataloader(Content))
+ end
+ end
+```
+
+This way associations are all handled in the context [business logic aware](https://github.com/absinthe-graphql/absinthe/issues/443#issuecomment-405929499) conditions, to support this is actually surprisingly simple.
+
+Since we had already setup users to load associated posts we can change that to use dataloader to illustrate how much simpler this gets.
+
+Let's start by adding `dataloader` as a dependency in `mix.exs`:
+
+```elixir
+defp deps do
+ [
+ {:dataloader, "~> 1.0.4"}
+ << other deps >>
+ ]
+```
+
+Next, we need to set up dataloader in our context which allows us to load associations using rules:
+
+In `lib/blog/content.ex`:
+
+```elixir
+ def data(), do: Dataloader.Ecto.new(Repo, query: &query/2)
+
+ def query(queryable, params) do
+
+ queryable
+ end
+```
+
+This sets up a loader that can use pattern matching to load different rules for different queryables, also note this function is passed in the context as the second parameter and that can be used for further filtering.
+
+Then let's add a configuration to our schema (in `lib/blog_web/schema.ex`) so that we can allow Absinthe to use Dataloader:
+
+```elixir
+defmodule BlogWeb.Schema do
+ use Absinthe.Schema
+
+ def context(ctx) do
+ loader =
+ Dataloader.new()
+ |> Dataloader.add_source(Content, Content.data())
+
+ Map.put(ctx, :loader, loader)
+ end
+
+ def plugins do
+ [Absinthe.Middleware.Dataloader | Absinthe.Plugin.defaults()]
+ end
+
+ # << rest of the file>>
+```
+
+The loader is all set up, now let's modify the resolver to use Dataloader. In `lib/blog_web/schema/account_types.ex` modify the user object to look as follows:
+
+```elixir
+@desc "A user of the blog"
+ object :user do
+ field :id, :id
+ field :name, :string
+ field :contacts, list_of(:contact)
+ field :posts, list_of(:post) do
+ arg :date, :date
+ resolve: dataloader(Content))
+ end
+ end
+```
+
+That's it! You are now loading associations using [Dataloader](https://github.com/absinthe-graphql/dataloader)
+
+## More Examples
+While the above examples are simple and straightforward we can use other strategies with loading associations consider the following:
+
+```elixir
+object :user do
+ field :posts, list_of(:post), resolve: fn user, args, %{context: %{loader: loader}} ->
+ loader
+ |> Dataloader.load(Blog, :posts, user)
+ |> on_load(fn loader ->
+ {:ok, Dataloader.get(loader, Blog, :posts, user)}
+ end)
+ end
+```
+
+In this example, we are passing some args go the query in the context where our source lives. For example, this function now receives `args` as `params` meaning we can do now do fun stuff like apply rules to our queries like the following:
+
+```elixir
+def query(query, %{has_admin_rights: true}), do: query
+
+def query(query, _), do: from(a in query, select_merge: %{street_number: nil})
+```
+
+This example is from the awesome [EmCasa Application](https://github.com/emcasa/backend/blob/master/apps/re/lib/addresses/addresses.ex) :) you can see how the [author](https://github.com/rhnonose) is only loading street numbers if a user has admin rights and the same used in a [resolver](https://github.com/emcasa/backend/blob/9a0f86c11499be6e1a07d0b0acf1785521eedf7f/apps/re_web/lib/graphql/resolvers/addresses.ex#L11).
+
+Check out the [docs](https://hexdocs.pm/dataloader/) for more non-trivial ways of using Dataloader.
diff --git a/tutorial/src/make-html-from-md.sh b/tutorial/src/make-html-from-md.sh
new file mode 100644
index 0000000..29e134b
--- /dev/null
+++ b/tutorial/src/make-html-from-md.sh
@@ -0,0 +1 @@
+find . -name \*.md -type f -exec pandoc -f markdown -t html -o ../{}.html `basename -s .md {}` \;
diff --git a/tutorial/src/mutations.md b/tutorial/src/mutations.md
new file mode 100644
index 0000000..eda0e98
--- /dev/null
+++ b/tutorial/src/mutations.md
@@ -0,0 +1,76 @@
+# Mutations
+
+A blog is no good without new content. We want to support a mutation
+to create a blog post:
+
+```graphql
+mutation CreatePost {
+ createPost(title: "Second", body: "We're off to a great start!") {
+ id
+ }
+}
+```
+
+Now we just need to define a `mutation` portion of our schema and
+a `:create_post` field:
+
+In `blog_web/schema.ex`:
+
+```elixir
+mutation do
+
+ @desc "Create a post"
+ field :create_post, type: :post do
+ arg :title, non_null(:string)
+ arg :body, non_null(:string)
+ arg :published_at, :naive_datetime
+
+ resolve &Resolvers.Content.create_post/3
+ end
+
+end
+```
+
+The resolver in this case is responsible for making any changes and
+returning an `{:ok, post}` tuple matching the `:post` type we defined
+earlier:
+
+In our `blog_web/resolvers/content.ex` module, we'll add the
+`create_post/3` resolver function:
+
+```elixir
+def create_post(_parent, args, %{context: %{current_user: user}}) do
+ Blog.Content.create_post(user, args)
+end
+def create_post(_parent, _args, _resolution) do
+ {:error, "Access denied"}
+end
+```
+
+> Obviously things can go wrong in a mutation. To learn more about the
+> types of error results that Absinthe supports, read [the guide](errors.html).
+
+## Authorization
+
+This resolver adds a new concept: authorization. The resolution struct
+(that is, an [`Absinthe.Resolution`](Absinthe.Resolution.html))
+passed to the resolver as the third argument carries along with it the
+Absinthe context, a data structure that serves as the integration
+point with external mechanisms---like a Plug that authenticates the
+current user. You can learn more about how the context can be used in
+the [Context and Authentication](context-and-authentication.html)
+guide.
+
+Going back to the resolver code:
+
+- If the match for a current user is successful, the underlying
+ `Blog.Content.create_post/2` function is invoked. It will return a
+ tuple suitable for return. (To read the Ecto-related nitty gritty,
+ check out the [absinthe_tutorial](https://github.com/absinthe-graphql/absinthe_tutorial)
+ repository.)
+- If the match for a current user isn't successful, the fall-through
+ match will return an error indicating that a post can't be created.
+
+## Next Step
+
+Now let's take a look at [more complex arguments](complex-arguments.html).
diff --git a/tutorial/src/our-first-query.md b/tutorial/src/our-first-query.md
new file mode 100644
index 0000000..088d309
--- /dev/null
+++ b/tutorial/src/our-first-query.md
@@ -0,0 +1,160 @@
+# Our First Query
+
+The first thing our viewers want is a list of our blog posts, so
+that's what we're going to give them. Here's the query we want to
+support:
+
+```graphql
+{
+ posts {
+ title
+ body
+ }
+}
+```
+
+To do this we're going to need a schema. Let's create some basic types
+for our schema, starting with a `:post`. GraphQL has several fundamental
+types on top of which all of our types will be
+built. The [Object](Absinthe.Type.Object.html) type is the right one
+to use when representing a set of key value pairs.
+
+Since our `Post` Ecto schema lives in the `Blog.Content` Phoenix
+context, we'll define its GraphQL counterpart type, `:post`, in a
+matching `BlogWeb.Schema.ContentTypes` module:
+
+In `blog_web/schema/content_types.ex`:
+
+```elixir
+defmodule BlogWeb.Schema.ContentTypes do
+ use Absinthe.Schema.Notation
+
+ object :post do
+ field :id, :id
+ field :title, :string
+ field :body, :string
+ end
+end
+```
+
+> The GraphQL specification requires that type names be unique, TitleCased words.
+> Absinthe does this automatically for us, extrapolating from our type identifier
+> (in this case `:post` gives us `"Post"`. If really needed, we could provide a
+> custom type name as a `:name` option to the `object` macro.
+
+If you're curious what the type `:id` is used by the `:id` field, see
+the [GraphQL spec](https://facebook.github.io/graphql/#sec-ID). It's
+an opaque value, and in our case is just the regular Ecto id, but
+serialized as a string.
+
+With our type completed we can now write a basic schema that will let
+us query a set of posts.
+
+In `blog_web/schema.ex`:
+
+```elixir
+defmodule BlogWeb.Schema do
+ use Absinthe.Schema
+ import_types BlogWeb.Schema.ContentTypes
+
+ alias BlogWeb.Resolvers
+
+ query do
+
+ @desc "Get all posts"
+ field :posts, list_of(:post) do
+ resolve &Resolvers.Content.list_posts/3
+ end
+
+ end
+
+end
+```
+
+> For more information on the macros available to build a schema, see
+> their definitions in [Absinthe.Schema](Absinthe.Schema.html) and
+> [Absinthe.Schema.Notation](Absinthe.Schema.Notation.html).
+
+This uses a resolver module we've created (again, to match the Phoenix context naming)
+at `blog_web/resolvers/content.ex`:
+
+```elixir
+defmodule BlogWeb.Resolvers.Content do
+
+ def list_posts(_parent, _args, _resolution) do
+ {:ok, Blog.Content.list_posts()}
+ end
+
+end
+```
+
+Queries are defined as fields inside the GraphQL object returned by
+our `query` function. We created a posts query that has a type
+`list_of(:post)` and is resolved by our
+`BlogWeb.Resolvers.Content.list_posts/3` function. Later we'll talk
+more about the resolver function parameters; for now just remember
+that resolver functions can take two forms:
+
+- A function with an arity of 3 (taking a parent, arguments, and resolution struct)
+- An alternate, short form with an arity of 2 (omitting the first parameter, the parent)
+
+The job of the resolver function is to return the data for the
+requested field. Our resolver calls out to the `Blog.Content` module,
+which is where all the domain logic for posts lives, invoking its
+`list_posts/0` function, then returns the posts in an `:ok` tuple.
+
+> Resolvers can return a wide variety of results, to include errors and configuration
+> for [advanced plugins](middleware-and-plugins.html) that further process the data.
+>
+> If you're asking yourself what the implementation of the domain logic looks like, and exactly how
+> the related Ecto schemas are built, read through the code in the [absinthe_tutorial](http://github.com/absinthe-graphql/absinthe_tutorial)
+> repository. The tutorial content here is intentionally focused on the Absinthe-specific code.
+
+Now that we have the functional pieces in place, let's configure our
+Phoenix router to wire this into HTTP:
+
+In `blog_web/router.ex`:
+
+```elixir
+defmodule BlogWeb.Router do
+ use BlogWeb, :router
+
+ pipeline :api do
+ plug :accepts, ["json"]
+ end
+
+ scope "/api" do
+ pipe_through :api
+
+ forward "/graphiql", Absinthe.Plug.GraphiQL,
+ schema: BlogWeb.Schema
+
+ forward "/", Absinthe.Plug,
+ schema: BlogWeb.Schema
+
+ end
+
+end
+```
+
+In addition to our API, we've wired in a handy GraphiQL user interface to play with it. Absinthe integrates both the classic [GraphiQL](https://github.com/graphql/graphiql) and more advanced [GraphiQL Workspace](https://github.com/OlegIlyenko/graphiql-workspace) interfaces as part of the [absinthe_plug](https://hex.pm/packages/absinthe_plug) package.
+
+Now let's check to make sure everything is working. Start the server:
+
+``` shell
+$ mix phx.server
+```
+
+Absinthe does a number of sanity checks during compilation, so if you misspell a type or make another schema-related gaffe, you'll be notified.
+
+Once it's up-and-running, take a look at [http://localhost:4000/api/graphiql](http://localhost:4000/api/graphiql):
+
+
+
+Make sure that the `URL` is pointing to the correct place and press the play button. If everything goes according to plan, you should see something like this:
+
+
+
+## Next Step
+
+Now let's look at how we can [add arguments to our queries](query-arguments.html).
diff --git a/tutorial/src/query-arguments.md b/tutorial/src/query-arguments.md
new file mode 100644
index 0000000..215d4d1
--- /dev/null
+++ b/tutorial/src/query-arguments.md
@@ -0,0 +1,268 @@
+# Query Arguments
+
+Our GraphQL API would be pretty boring (and useless) if clients
+couldn't retrieve filtered data.
+
+Let's assume that our API needs to add the ability to look-up users by
+their ID and get the posts that they've authored. Here's what a basic query to do that
+might look like:
+
+```graphql
+{
+ user(id: "1") {
+ name
+ posts {
+ id
+ title
+ }
+ }
+}
+```
+
+The query includes a field argument, `id`, contained within the
+parentheses after the `user` field name. To make this all work, we need to modify
+our schema a bit.
+
+## Defining Arguments
+
+First, let's create a `:user` type and define its relationship to
+`:post` while we're at it. We'll create a new module for the
+account-related types and put it there; in
+`blog_web/schema/account_types.ex`:
+
+```elixir
+defmodule BlogWeb.Schema.AccountTypes do
+ use Absinthe.Schema.Notation
+
+ @desc "A user of the blog"
+ object :user do
+ field :id, :id
+ field :name, :string
+ field :email, :string
+ field :posts, list_of(:post)
+ end
+
+end
+```
+
+The `:posts` field points to a list of `:post` results. (This matches
+up with what we have on the Ecto side, where `Blog.Accounts.User`
+defines a `has_many` association with `Blog.Content.Post`.)
+
+We've already defined the `:post` type, but let's go ahead and add an
+`:author` field that points back to our `:user` type. In
+`blog_web/schema/content_types.ex`:
+
+``` elixir
+object :post do
+
+ # post fields we defined earlier...
+
+ field :author, :user
+
+end
+```
+
+Now let's add the `:user` field to our query root object in our
+schema, defining a mandatory `:id` argument and using the
+`Resolvers.Accounts.find_user/3` resolver function. We also need to
+make sure we import the types from `BlogWeb.Schema.AccountTypes` so
+that `:user` is available.
+
+In `blog_web/schema.ex`:
+
+```elixir
+defmodule BlogWeb.Schema do
+ use Absinthe.Schema
+
+ import_types Absinthe.Type.Custom
+
+ # Add this `import_types`:
+ import_types BlogWeb.Schema.AccountTypes
+
+ import_types BlogWeb.Schema.ContentTypes
+
+ alias BlogWeb.Resolvers
+
+ query do
+
+ @desc "Get all posts"
+ field :posts, list_of(:post) do
+ resolve &Resolvers.Content.list_posts/3
+ end
+
+ # Add this field:
+ @desc "Get a user of the blog"
+ field :user, :user do
+ arg :id, non_null(:id)
+ resolve &Resolvers.Accounts.find_user/3
+ end
+
+ end
+
+end
+```
+
+Now lets use the argument in our resolver. In `blog_web/resolvers/accounts.ex`:
+
+```elixir
+defmodule BlogWeb.Resolvers.Accounts do
+
+ def find_user(_parent, %{id: id}, _resolution) do
+ case Blog.Accounts.find_user(id) do
+ nil ->
+ {:error, "User ID #{id} not found"}
+ user ->
+ {:ok, user}
+ end
+ end
+
+end
+```
+
+Our schema marks the `:id` argument as `non_null`, so we can be
+certain we will receive it. If `:id` is left out of the query,
+Absinthe will return an informative error to the user, and the resolve
+function will not be called.
+
+> If you have experience writing Phoenix controller actions, you might
+> wonder why we can match incoming arguments with atoms instead of
+> having to use strings.
+>
+> The answer is simple: you've defined the arguments in the schema
+> using atom identifiers, so Absinthe knows what arguments will be
+> used ahead of time, and will coerce as appropriate---culling any
+> extraneous arguments given to a query. This means that all arguments
+> can be supplied to the resolve functions with atom keys.
+
+Finally you'll see that we can handle the possibility that the query,
+while valid from GraphQL's perspective, may still ask for a user that
+does not exist. We've decided to return an error in that case.
+
+> There's a valid argument for just returning `{:ok, nil}` when a
+> record can't be found. Whether the absence of data constitutes an
+> error is a decision you get to make.
+
+## Arguments and Non-Root Fields
+
+Let's assume we want to query all posts from a user published within a
+given time range. First, let's add a new field to our `:post` object
+type, `:published_at`.
+
+The GraphQL specification doesn't define any official date or time
+types, but it does support custom scalar types (you can read more
+about them in the [related guide](custom-scalars.html), and
+Absinthe ships with several built-in scalar types. We'll use
+`:naive_datetime` (which doesn't include timezone information) here.
+
+Edit `blog_web/schema/content_types.ex`:
+
+```elixir
+defmodule BlogWeb.Schema.ContentTypes do
+ use Absinthe.Schema.Notation
+
+ @desc "A blog post"
+ object :post do
+ field :id, :id
+ field :title, :string
+ field :body, :string
+ field :author, :user
+ # Add this:
+ field :published_at, :naive_datetime
+ end
+end
+```
+
+To make the `:naive_datetime` type available, add an `import_types` line to
+your `blog_web/schema.ex`:
+
+``` elixir
+import_types Absinthe.Type.Custom
+```
+
+> For more information about how types are imported,
+> read [the guide on the topic](importing-types.html).
+>
+> For now, just remember that `import_types` should _only_ be
+> used in top-level schema module. (Think of it like a manifest.)
+
+Here's the query we'd like to be able to use, getting the posts for a user
+on a given date:
+
+```graphql
+{
+ user(id: "1") {
+ name
+ posts(date: "2017-01-01") {
+ title
+ body
+ publishedAt
+ }
+ }
+}
+```
+
+To use the passed date, we need to update our `:user` object type and
+make some changes to its `:posts` field; it needs to support a `:date`
+argument and use a custom resolver. In `blog_web/schema/account_types.ex`:
+
+```elixir
+defmodule BlogWeb.Schema.AccountTypes do
+ use Absinthe.Schema.Notation
+
+ alias BlogWeb.Resolvers
+
+ object :user do
+ field :id, :id
+ field :name, :string
+ field :email, :string
+ # Add the block here:
+ field :posts, list_of(:post) do
+ arg :date, :date
+ resolve &Resolvers.Content.list_posts/3
+ end
+ end
+
+end
+```
+
+For the resolver, we've added another function head to
+`Resolvers.Content.list_posts/3`. This illustrates how you can use the
+first argument to a resolver to match the parent object of a field. In
+this case, that parent object would be a `Blog.Accounts.User` Ecto
+schema:
+
+``` elixir
+# Add this:
+def list_posts(%Blog.Accounts.User{} = author, args, _resolution) do
+ {:ok, Blog.Content.list_posts(author, args)}
+end
+# Before this:
+def list_posts(_parent, _args, _resolution) do
+ {:ok, Blog.Content.list_posts()}
+end
+```
+
+Here we pass on the user and arguments to the domain logic function,
+`Blog.Content.list_posts/3`, which will find the posts for the user
+and date (if it's provided; the `:date` argument is optional). The
+resolver, just as when it's used for the top level query `:posts`,
+returns the posts in an `:ok` tuple.
+
+> Check out the full implementation of logic for
+> `Blog.Content.list_posts/3`--and some simple seed data--in
+> the
+> [absinthe_tutorial](https://github.com/absinthe-graphql/absinthe_tutorial) repository.
+
+If you've done everything correctly (and have some data handy), if you
+start up your server with `mix phx.server` and head over
+to
+
+## Next Step
+
+Next up, we look at how to modify our data using [mutations](mutations.html).
diff --git a/tutorial/src/start.md b/tutorial/src/start.md
new file mode 100644
index 0000000..eb25450
--- /dev/null
+++ b/tutorial/src/start.md
@@ -0,0 +1,23 @@
+# Getting Started
+
+We'll be building a very basic GraphQL API for a blog, written in Elixir using
+Absinthe.
+
+## Background
+
+Before you start, it's a good idea to have some background into GraphQL in general. Here are a few resources that might be helpful:
+
+- The official [GraphQL](http://graphql.org/) website
+- [How to GraphQL](https://www.howtographql.com/) (this includes another [brief tutorial](https://www.howtographql.com/graphql-elixir/0-introduction/) using Absinthe)
+
+## The Example
+
+ The tutorial expects you to have a properly set-up [Phoenix application](https://hexdocs.pm/phoenix/installation.html) with absinthe and absinthe_plug added to the dependencies.
+
+> If you'd like to cheat, you can find the finished code for the tutorial
+> in the Absinthe Example
+> project on GitHub.
+
+## First Step
+
+Let's get started with [our first query](our-first-query.html)!
diff --git a/tutorial/src/subscriptions.md b/tutorial/src/subscriptions.md
new file mode 100644
index 0000000..f9c22c5
--- /dev/null
+++ b/tutorial/src/subscriptions.md
@@ -0,0 +1,147 @@
+# Subscriptions
+
+When the need arises for near realtime data GraphQL provides subscriptions. We want to support subscriptions that look like
+
+
+```graphql
+subscription{
+ newPost {
+ id
+ name
+ }
+}
+```
+
+Since we had already setup mutations to handle creation of posts we can use that as the event we want to subscribe to. In order to achieve this we have to do a little bit of set up
+
+
+Let's start by adding `absinthe_phoenix` as a dependency
+
+In `mix.exs`
+
+```elixir
+defp deps do
+ [
+ {:absinthe_phoenix, "~> 1.4.0"}
+ << other deps >>
+ ]
+```
+
+Then we need to add a supervisor to run some processes for the to handle result broadcasts
+
+In `lib/blog/application.ex`:
+
+```elixir
+ children = [
+ # other children ...
+ {BlogWeb.Endpoint, []}, # this line should already exist
+ {Absinthe.Subscription, [BlogWeb.Endpoint]}, # add this line
+ # other children ...
+ ]
+```
+
+
+
+The lets add a configuration to the phoenix endpoint so it can provide some callbacks Absinthe expects, please note while this guide uses phoenix. Absinthe's support for Subscriptions is good enough to be used without websockets even without a browser.
+
+In `lib/blog_web/endpoint.ex`:
+
+
+```elixir
+defmodule BlogWeb.Endpoint do
+ use Phoenix.Endpoint, otp_app: :blog # this line should already exist
+ use Absinthe.Phoenix.Endpoint # add this line
+
+ << rest of the file>>
+```
+
+The `PubSub` stuff is now set up, let's configure our sockets
+
+In `lib/blog_web/channels/user_socket.ex`
+
+``` elixir
+defmodule BlogWeb.UserSocket do
+ use Phoenix.Socket # this line should already exist
+ use Absinthe.Phoenix.Socket, schema: BlogWeb.Schema # add
+
+ << rest of file>>
+```
+
+Lets now configure GraphQL to use this Socket.
+
+In `lib/blog_web/router.ex` :
+
+```elixir
+defmodule BlogWeb.Router do
+ use BlogWeb, :router
+
+ pipeline :api do
+ plug :accepts, ["json"]
+ plug BlogWeb.Context
+ end
+
+ scope "/api" do
+ pipe_through :api
+
+ forward "/graphiql", Absinthe.Plug.GraphiQL,
+ schema: BlogWeb.Schema,
+ socket: BlogWeb.UserSocket # add this line
+
+
+ forward "/", Absinthe.Plug,
+ schema: BlogWeb.Schema
+ end
+
+end
+```
+
+
+Now let/s set up a subscription root object in our Schema to listen for an event. For this subscription we can set it up to listen every time a new post is created.
+
+
+In `blog_web/schema.ex` :
+
+```elixir
+
+subscription do
+
+ field :new_post, :post do
+ config fn _args, _info ->
+ {:ok, topic: "*"}
+ end
+ end
+
+end
+```
+
+The `new_post` field is a pretty regular field only new thing here is the `config` macro, this is
+here to help us know which clients have subscribed to which fields. Much like WebSockets subscriptions work by allowing t a client to subscribe to a topic.
+
+Topics are scoped to a field and for now we shall use `*` to indicate we care about all the posts, and that's it!
+
+If you ran the request at this moment you would get a nice message telling you that your subscriptions will appear once after they are published but you create a post and alas! no data what cut?
+
+Once a subscription is set up it waits for a target event to get published in order for us to collect this information we need to publish to this subscription
+
+In `blog_web/resolvers/content.ex`:
+
+```elixir
+def create_post(_parent, args, %{context: %{current_user: user}}) do
+ # Blog.Content.create_post(user, args)
+ case Blog.Content.create_post(user, args) do
+ {:ok, post} ->
+ # add this line in
+ Absinthe.Subscription.publish(BlogWeb.Endpoint, post,
+ new_post: "*"
+ )
+
+ {:ok, post}
+ {:error, changeset} ->
+ {:ok, "error"}
+ end
+ end
+```
+
+With this, open a tab and run the query at the top of this section. Then open another tab and run a mutation to add a post you should see a result in the other tab have fun.
+
+
diff --git a/tutorial/start.html b/tutorial/start.html
new file mode 100644
index 0000000..6f5ef1d
--- /dev/null
+++ b/tutorial/start.html
@@ -0,0 +1,15 @@
+We'll be building a very basic GraphQL API for a blog, written in Elixir using Absinthe.
+Before you start, it's a good idea to have some background into GraphQL in general. Here are a few resources that might be helpful:
+The tutorial expects you to have a properly set-up Phoenix application with absinthe and absinthe_plug added to the dependencies.
+++If you'd like to cheat, you can find the finished code for the tutorial in the Absinthe Example project on GitHub.
+
Let's get started with our first query!
diff --git a/tutorial/start.md.html b/tutorial/start.md.html new file mode 100644 index 0000000..6f5ef1d --- /dev/null +++ b/tutorial/start.md.html @@ -0,0 +1,15 @@ +We'll be building a very basic GraphQL API for a blog, written in Elixir using Absinthe.
+Before you start, it's a good idea to have some background into GraphQL in general. Here are a few resources that might be helpful:
+The tutorial expects you to have a properly set-up Phoenix application with absinthe and absinthe_plug added to the dependencies.
+++If you'd like to cheat, you can find the finished code for the tutorial in the Absinthe Example project on GitHub.
+
Let's get started with our first query!
diff --git a/tutorial/subscriptions.html b/tutorial/subscriptions.html new file mode 100644 index 0000000..66d80d5 --- /dev/null +++ b/tutorial/subscriptions.html @@ -0,0 +1,94 @@ +When the need arises for near realtime data GraphQL provides subscriptions. We want to support subscriptions that look like
+subscription{
+ newPost {
+ id
+ name
+ }
+}
+Since we had already setup mutations to handle creation of posts we can use that as the event we want to subscribe to. In order to achieve this we have to do a little bit of set up
+Let's start by adding absinthe_phoenix as a dependency
In mix.exs
defp deps do
+ [
+ {:absinthe_phoenix, "~> 1.4.0"}
+ << other deps >>
+ ]
+Then we need to add a supervisor to run some processes for the to handle result broadcasts
+In lib/blog/application.ex:
children = [
+ # other children ...
+ {BlogWeb.Endpoint, []}, # this line should already exist
+ {Absinthe.Subscription, [BlogWeb.Endpoint]}, # add this line
+ # other children ...
+ ]
+The lets add a configuration to the phoenix endpoint so it can provide some callbacks Absinthe expects, please note while this guide uses phoenix. Absinthe's support for Subscriptions is good enough to be used without websockets even without a browser.
+In lib/blog_web/endpoint.ex:
defmodule BlogWeb.Endpoint do
+ use Phoenix.Endpoint, otp_app: :blog # this line should already exist
+ use Absinthe.Phoenix.Endpoint # add this line
+
+ << rest of the file>>
+The PubSub stuff is now set up, let's configure our sockets
In lib/blog_web/channels/user_socket.ex
defmodule BlogWeb.UserSocket do
+ use Phoenix.Socket # this line should already exist
+ use Absinthe.Phoenix.Socket, schema: BlogWeb.Schema # add
+
+ << rest of file>>
+Lets now configure GraphQL to use this Socket.
+In lib/blog_web/router.ex :
defmodule BlogWeb.Router do
+ use BlogWeb, :router
+
+ pipeline :api do
+ plug :accepts, ["json"]
+ plug BlogWeb.Context
+ end
+
+ scope "/api" do
+ pipe_through :api
+
+ forward "/graphiql", Absinthe.Plug.GraphiQL,
+ schema: BlogWeb.Schema,
+ socket: BlogWeb.UserSocket # add this line
+
+
+ forward "/", Absinthe.Plug,
+ schema: BlogWeb.Schema
+ end
+
+end
+Now let/s set up a subscription root object in our Schema to listen for an event. For this subscription we can set it up to listen every time a new post is created.
+In blog_web/schema.ex :
+subscription do
+
+ field :new_post, :post do
+ config fn _args, _info ->
+ {:ok, topic: "*"}
+ end
+ end
+
+end
+The new_post field is a pretty regular field only new thing here is the config macro, this is here to help us know which clients have subscribed to which fields. Much like WebSockets subscriptions work by allowing t a client to subscribe to a topic.
Topics are scoped to a field and for now we shall use * to indicate we care about all the posts, and that's it!
If you ran the request at this moment you would get a nice message telling you that your subscriptions will appear once after they are published but you create a post and alas! no data what cut?
+Once a subscription is set up it waits for a target event to get published in order for us to collect this information we need to publish to this subscription
+In blog_web/resolvers/content.ex:
def create_post(_parent, args, %{context: %{current_user: user}}) do
+ # Blog.Content.create_post(user, args)
+ case Blog.Content.create_post(user, args) do
+ {:ok, post} ->
+ # add this line in
+ Absinthe.Subscription.publish(BlogWeb.Endpoint, post,
+ new_post: "*"
+ )
+
+ {:ok, post}
+ {:error, changeset} ->
+ {:ok, "error"}
+ end
+ end
+With this, open a tab and run the query at the top of this section. Then open another tab and run a mutation to add a post you should see a result in the other tab have fun.
+
When the need arises for near realtime data GraphQL provides subscriptions. We want to support subscriptions that look like
+subscription{
+ newPost {
+ id
+ name
+ }
+}
+Since we had already setup mutations to handle creation of posts we can use that as the event we want to subscribe to. In order to achieve this we have to do a little bit of set up
+Let's start by adding absinthe_phoenix as a dependency
In mix.exs
defp deps do
+ [
+ {:absinthe_phoenix, "~> 1.4.0"}
+ << other deps >>
+ ]
+Then we need to add a supervisor to run some processes for the to handle result broadcasts
+In lib/blog/application.ex:
children = [
+ # other children ...
+ {BlogWeb.Endpoint, []}, # this line should already exist
+ {Absinthe.Subscription, [BlogWeb.Endpoint]}, # add this line
+ # other children ...
+ ]
+The lets add a configuration to the phoenix endpoint so it can provide some callbacks Absinthe expects, please note while this guide uses phoenix. Absinthe's support for Subscriptions is good enough to be used without websockets even without a browser.
+In lib/blog_web/endpoint.ex:
defmodule BlogWeb.Endpoint do
+ use Phoenix.Endpoint, otp_app: :blog # this line should already exist
+ use Absinthe.Phoenix.Endpoint # add this line
+
+ << rest of the file>>
+The PubSub stuff is now set up, let's configure our sockets
In lib/blog_web/channels/user_socket.ex
defmodule BlogWeb.UserSocket do
+ use Phoenix.Socket # this line should already exist
+ use Absinthe.Phoenix.Socket, schema: BlogWeb.Schema # add
+
+ << rest of file>>
+Lets now configure GraphQL to use this Socket.
+In lib/blog_web/router.ex :
defmodule BlogWeb.Router do
+ use BlogWeb, :router
+
+ pipeline :api do
+ plug :accepts, ["json"]
+ plug BlogWeb.Context
+ end
+
+ scope "/api" do
+ pipe_through :api
+
+ forward "/graphiql", Absinthe.Plug.GraphiQL,
+ schema: BlogWeb.Schema,
+ socket: BlogWeb.UserSocket # add this line
+
+
+ forward "/", Absinthe.Plug,
+ schema: BlogWeb.Schema
+ end
+
+end
+Now let/s set up a subscription root object in our Schema to listen for an event. For this subscription we can set it up to listen every time a new post is created.
+In blog_web/schema.ex :
+subscription do
+
+ field :new_post, :post do
+ config fn _args, _info ->
+ {:ok, topic: "*"}
+ end
+ end
+
+end
+The new_post field is a pretty regular field only new thing here is the config macro, this is here to help us know which clients have subscribed to which fields. Much like WebSockets subscriptions work by allowing t a client to subscribe to a topic.
Topics are scoped to a field and for now we shall use * to indicate we care about all the posts, and that's it!
If you ran the request at this moment you would get a nice message telling you that your subscriptions will appear once after they are published but you create a post and alas! no data what cut?
+Once a subscription is set up it waits for a target event to get published in order for us to collect this information we need to publish to this subscription
+In blog_web/resolvers/content.ex:
def create_post(_parent, args, %{context: %{current_user: user}}) do
+ # Blog.Content.create_post(user, args)
+ case Blog.Content.create_post(user, args) do
+ {:ok, post} ->
+ # add this line in
+ Absinthe.Subscription.publish(BlogWeb.Endpoint, post,
+ new_post: "*"
+ )
+
+ {:ok, post}
+ {:error, changeset} ->
+ {:ok, "error"}
+ end
+ end
+With this, open a tab and run the query at the top of this section. Then open another tab and run a mutation to add a post you should see a result in the other tab have fun.
+