0

I have created a Phoenix app in Elixir. I used the generation tool to create all the crud functionality and the crud pages.

It currently uses:

def index(conn, _params) do
  data = Repo.all(Object)
  render(conn, "index.html", data: data)
end

How do I replace this with a GraphQL implementation, because I currently have the ability to pass GraphQL queries via a specified url, eg. getting all the records from a table. The documentation talks about using the absinthe_phoenix plug and adding that to your pipeline. This ends up just replacing the current web pages that I have and asks for a url, all the current pages being the ones created by Phoenix when you run the scaffolding command to generate the crud and the database schema.

I need to keep all those crud pages but have them run GrapQL queries. So on the page that displays all the records from the database, I need it to instead of running

data = Repo.all(Object)

it should run

{
  objects{
    field1,
    field2
  }
}

to get all the data. How do I run GraphQL queries in the controllers?

This is the query that I need to run in my GraphQL schema

query do
    @doc """
    Returns all the records from a database
    """
    field :objects, list_of(:object) do
        resolve &Billingplus.ObjectResolver.all/2
    end
end
marc_s
  • 732,580
  • 175
  • 1,330
  • 1,459
Errol Hassall
  • 398
  • 3
  • 16
  • Why not create one global GraphQL route to handle all queries and mutations (with `absinthe_plug`) instead of adding GraphQL functionality to each controller with `absinthe_phoenix`? – zwippie Feb 28 '18 at 13:38
  • See thats what I already have, I can use a specific url to send GraphQL queries currently. But how can I get my controllers to use that is my question. Or is it better to let it talk directly to the Ecto.Repo? – Errol Hassall Feb 28 '18 at 22:38
  • That is correct, your REST controllers should not use graphql queries but just use Ecto to fetch data from the DB. – zwippie Mar 01 '18 at 13:14
  • Interesting, what is the reasoning behind that? Is that because that would be overcomplicating it for no real benefit? – Errol Hassall Mar 01 '18 at 21:49
  • Exactly. GraphQL and REST are different paradigms, there's no gain in mixing the one with the other. – zwippie Mar 01 '18 at 22:47

1 Answers1

0

You need to create references in your GQL schema, and have resolver functions for different things. You don't put it in your controller. I'll show you a front to back example of a "thing" implemented from DB to Absinthe.

REST and GQL are different paradigms/interfaces.

Do you have any working GQL in your project yet? Show what you've done so we can give advice.

Example mutation:

mutation do
    @desc "Create an OAuth2 client"
    field :create_oauth2_client, :oauth2_client do
      arg(:app_id, non_null(:uuid4))
      arg(:client_id, non_null(:string))
      arg(:client_secret, non_null(:string))
      arg(:oauth2_provider_id, non_null(:uuid4))

      resolve(&Resolvers.OAuth2Client.create_oauth2_client/3)
    end
end

Example query:

query do
    @desc "Get an OAuth2 client"
    field :oauth2_client, :oauth2_client do
      arg(:id, non_null(:uuid4))
      resolve(&Resolvers.OAuth2Client.get_oauth2_client/3)
    end
end

example schema object:

defmodule ApiWeb.Schema.OAuth2Client do
  use Absinthe.Schema.Notation

  alias Api.Auth.Apps
  alias Api.Auth.OAuth2Providers
  alias Api.Util

  @desc "An OAuth2 client"
  object :oauth2_client do
    field(:id, :uuid4)
    field(:client_id, :string)
    field(:client_secret, :string)

    field(:app, :app,
      resolve: fn oauth2_client, _, _ ->
        Apps.get_app(oauth2_client.app_id)
        |> Util.handle_not_found_error_and_wrap("App not found.")
      end
    )

    field(:oauth2_provider, :oauth2_provider,
      resolve: fn oauth2_client, _, _ ->
        OAuth2Providers.get_oauth2_provider(oauth2_client.oauth2_provider_id)
        |> Util.handle_not_found_error_and_wrap("OAuth2Provider not found.")
      end
    )
  end
end

example resolver:

defmodule ApiWeb.Resolvers.OAuth2Client do
  alias Api.Auth.OAuth2Clients

  #
  # QUERIES
  #
  def get_oauth2_client(_parent, %{id: id}, _resolution) do
    OAuth2Clients.get_oauth2_client(id)
  end

  def get_oauth2_clients_by_app(_parent, %{app_id: app_id}, _resolution) do
    OAuth2Clients.get_oauth2_clients_by_app(app_id)
  end

  #
  # MUTATIONS
  #
  def create_oauth2_client(_parent, params, _resolution) do
    OAuth2Clients.create_oauth2_client(%{
      app_id: params.app_id,
      oauth2_provider_id: params.oauth2_provider_id,
      client_id: params.client_id,
      client_secret: params.client_secret
    })
  end
end

Example context:

defmodule Api.Auth.OAuth2Clients do
  @moduledoc """
  The OAuth2Clients context.
  """

  import Ecto.Query, warn: false

  alias Api.Repo
  alias Api.Auth.OAuth2Clients.OAuth2Client
  alias Api.Util

  @doc """
  Returns the list of OAuth2Clients.

  ## Examples

      iex> list_oauth2_clients()
      {:ok, [%OAuth2Client{}, ...]}

  """
  def list_oauth2_clients do
    Repo.all(OAuth2Client)
    |> Util.handle_not_found_error_and_wrap("No OAuth2Clients found.")
  end

  @doc """
  Gets a single OAuth2Client.

  Returns `{:error, %NotFoundError{}}` if the OAuth2Client does not exist.

  ## Examples

      iex> get_oauth2_client(123)
      {:ok, %OAuth2Client{}}

      iex> get_oauth2_client(456)
      {:error, %NotFoundError{}}

  """
  def get_oauth2_client(id) do
    Repo.get(OAuth2Client, id)
    |> Util.handle_not_found_error_and_wrap("OAuth2Client not found.")
  end

  @doc """
  Gets a single OAuth2Client by `client_id` and `provider_id`.

  Returns `{:error, %NotFoundError{}}` if the OAuth2Client does not exist.

  ## Examples

      iex> get_oauth2_client_by_client_and_provider_id(123)
      {:ok, %OAuth2Client{}}

      iex> get_oauth2_client_by_client_and_provider_id(456)
      {:error, %NotFoundError{}}

  """
  def get_oauth2_client_by_client_and_provider_id(client_id, provider_id) do
    from(o in OAuth2Client,
      where: o.client_id == ^client_id and o.oauth2_provider_id == ^provider_id
    )
    |> Repo.one()
    |> Util.handle_not_found_error_and_wrap("OAuth2Client not found.")
  end

  @doc """
  Gets a list of OAuth2Client by App.

  Returns `{:error, %NotFoundError{}}` if there are no OAuth2Client to return.

  ## Examples

      iex> get_oauth2_clients_by_app(123)
      {:ok, [%OAuth2Client{}, ...]}

      iex> get_oauth2_clients_by_app(456)
      {:error, %NotFoundError{}}

  """
  def get_oauth2_clients_by_app(app_id) do
    from(o in OAuth2Client,
      where: o.app_id == ^app_id
    )
    |> Repo.all()
    |> Util.handle_not_found_error_and_wrap("App not found.")
  end

  @doc """
  Gets an OAuth2Client by `App` and `Provider`

  Returns `{:error, %NotFoundError{}}` if there is no `OAuth2Client` to return.

  ## Examples

      iex> get_oauth2_clients_by_app_and_provider(123)
      {:ok, [%OAuth2Client{}, ...]}

      iex> get_oauth2_clients_by_app_and_provider(456)
      {:error, %NotFoundError{}}

  """
  def get_oauth2_client_by_app_and_provider(app_id, provider_id) do
    from(o in OAuth2Client,
      where: o.app_id == ^app_id and o.oauth2_provider_id == ^provider_id
    )
    |> Repo.one()
    |> Util.handle_not_found_error_and_wrap("App not found.")
  end

  @doc """
  Creates an OAuth2Client.

  ## Examples

      iex> create_oauth2_client(%{field: value})
      {:ok, %OAuth2Client{}}

      iex> create_oauth2_client(%{field: bad_value})
      {:error, %Ecto.Changeset{}}

  """
  def create_oauth2_client(attrs) do
    %OAuth2Client{}
    |> OAuth2Client.create(attrs)
    |> Repo.insert()
  end

  @doc """
  Updates an OAuth2Client.

  ## Examples

      iex> update_oauth2_client(oauth2_client, %{field: new_value})
      {:ok, %OAuth2Client{}}

      iex> update_oauth2_client(oauth2_client, %{field: bad_value})
      {:error, %Ecto.Changeset{}}

  """
  def update_oauth2_client(%OAuth2Client{} = oauth2_client, attrs) do
    oauth2_client
    |> OAuth2Client.update(attrs)
    |> Repo.update()
  end

  @doc """
  Deletes a OAuth2Client.

  ## Examples

      iex> delete_oauth2_client(oauth2_client)
      {:ok, %OAuth2Client{}}

      iex> delete_oauth2_client(oauth2_client)
      {:error, %Ecto.Changeset{}}

  """
  def delete_oauth2_client(%OAuth2Client{} = oauth2_client) do
    Repo.delete(oauth2_client)
  end
end

Example schema/model:

defmodule Api.Auth.OAuth2Clients.OAuth2Client do
  use Ecto.Schema
  import Ecto.Changeset

  alias Api.Auth.Apps.App
  alias Api.Auth.OAuth2Providers.OAuth2Provider

  @primary_key {:id, :binary_id, autogenerate: true}
  @foreign_key_type :binary_id
  schema "oauth2_clients" do
    field(:client_id, :string)
    field(:client_secret, :string)

    belongs_to(:app, App)
    belongs_to(:oauth2_provider, OAuth2Provider)

    timestamps()
  end

  def create(app, attrs) do
    app
    |> cast(attrs, [:client_id, :client_secret, :oauth2_provider_id, :app_id])
    |> validate_required([:client_id, :client_secret, :oauth2_provider_id, :app_id])
    |> foreign_key_constraint(:app_id)
    |> foreign_key_constraint(:oauth2_provider_id)
  end

  def update(app, attrs) do
    app
    |> cast(attrs, [:client_id, :client_secret, :oauth2_provider_id, :app_id])
    |> foreign_key_constraint(:app_id)
    |> foreign_key_constraint(:oauth2_provider_id)
  end
end

example migration:

defmodule Api.Repo.Migrations.CreateOAuth2Clients do
  use Ecto.Migration

  def change do
    create table(:oauth2_clients, primary_key: false) do
      add(:id, :binary_id, primary_key: true)
      add(:client_id, :string, null: false)
      add(:client_secret, :string, null: false)
      add(:oauth2_provider_id, references(:oauth2_providers, type: :binary_id), null: false)
      add(:app_id, references(:apps, type: :binary_id), null: false)
      timestamps()
    end
  end

  def up do
    create(
      constraint(:owners, :user_or_organization,
        check:
          "((organization_id is not null and user_id is null) or (organization_id is null and user_id is not null))"
      )
    )
  end

  def down do
    drop(constraint(:owners, :user_or_organization))
  end
end

This is where you're getting confused. Instead of referencing a controller in your router, you specify a GQL endpoint and query your back end there.

defmodule ApiWeb.Router do
  use ApiWeb, :router

  alias ApiWeb.OAuthController

  pipeline :api do
    plug(Plug.Parsers,
      parsers: [:json, Absinthe.Plug.Parser],
      pass: ["*/*"],
      json_decoder: Jason
    )

    plug(:accepts, ["json"])
  end

  scope "/" do
    pipe_through(:api)

    get("/oauth2", OAuthController, :callback)

    post("/graphql", Absinthe.Plug, schema: ApiWeb.Schema)

    forward("/graphiql", Absinthe.Plug.GraphiQL,
      schema: ApiWeb.Schema,
      json_codec: Jason
    )
  end
end

There are a lot of different components to implementing Absinthe in a Phoenix project. It took me a while to wrap my head around it - mostly because the object definition is a little strange, and you can make pseudo virtual fields. Implementing the ability to resolve struct references inside of a query was also a little confusing at first.

Ian
  • 544
  • 3
  • 16