ash_graphql/documentation/topics/graphql-generation.md
Riccardo Binetti 513c1ac68f
improvement!: port AshGraphql to Ash 3.0 (#123)
Step 1: update Ash

Step 2: mass rename Api to Domain

Step 3: Ash.Query.expr -> Ash.Expr.expr

Also change ref interpolation

Step 4: remove all warnings

Step 5: remove registries from tests

Step 6: fix filter

Step 7: private? -> !public?

Step 8: Ash.Calculation -> Ash.Resource.Calculation

Step 9: use depend_on_resources/1 -> resources/1

Step 10: add Domain to all resources

Step 11: use Ash module for all actions

Step 12: add public? true all around

Step 13: remove verbose? from options passed during Domain calls

Step 14: add simple_sat

Step 15: Ash.ErrorKind is no more, so remove code from errors

Step 16: sprinkle default_accept :* around tests

Step 17: replace Ash.Changeset.new/2 with Ash.Changeset.for_*

Step 18: calculation fixups

- Context is now a struct and arguments go under the arguments key
- Function based calculations receive a list of records
- Add a select to query-based loads
- select -> load

Step 19: pass the correct name to pass the policy in tests

Step 20: Ash.Query.new/2 is no more

Step 21: add AshGraphql.Resource.embedded? utility function

Use that instead of Ash.Type.embedded_type?(resource_or_type) since resources
are not types anymore

Step 22: handle struct + instance_of: Resource in unions

Resources are not type anymore so they need to be passed this way in unions

Step 23: ensure we only check GraphQL actions for pagination

All reads are now paginated by default, so this triggered a compilation error

Step 24: swap arguments for sort on calculations

Step 25: remove unused debug? option
2024-04-01 14:03:06 -04:00

6.9 KiB

GraphQL Query Generation

Fetch Data by ID

Following where we left off from Getting Started with GraphQL, we'll explore what the GraphQL requests and responses look like for different queries defined with the AshGraphql DSL.

defmodule Helpdesk.Support.Ticket do
  use Ash.Resource,
    ...,
    extensions: [
      AshGraphql.Resource
    ]

  attributes do
    # Add an autogenerated UUID primary key called `:id`.
    uuid_primary_key :id

    # Add a string type attribute called `:subject`
    attribute :subject, :string
  end

  actions do
    # Add a set of simple actions. You'll customize these later.
    defaults [:read, :update, :destroy]
  end

  graphql do
    type :ticket

    queries do
      # create a field called `get_ticket` that uses the `read` read action to fetch a single ticket
      get :get_ticket, :read 
    end
  end
end

For the get_ticket query defined above, the corresponding GraphQL would look like this:

query($id: ID!) {
  getTicket(id: $id) {
    id
    subject
  }
}

And the response would look similar to this:

{
  "data": {
    "getTicket": {
      "id": "",
      "subject": ""
    }
  }
}

Let's look at an example of querying a list of things.

  graphql do
    type :ticket

    queries do
      # create a field called `get_ticket` that uses the `read` read action to fetch a single ticket
      get :get_ticket, :read 

      # create a field called `list_tickets` that uses the `read` read action to fetch a list of tickets
      list :list_tickets, :read 
    end
  end

This time, we've added list :list_tickets, :read, to generate a GraphQL query for listing tickets. The request would look something like this:

query {
  listTickets {
    id
    subject
  }
}

And the response would look similar to this:

{
  "data": {
    "listTickets": [
      {
        "id": "",
        "subject": ""
      }
    ]
  }
}

Filter Data With Arguments

Now, let's say we want to add query parameters to listTickets. How do we do that? Consider list :list_tickets, :read and the actions section:

  actions do
    # Add a set of simple actions. You'll customize these later.
    defaults [:read, :update, :destroy]
  end

  graphql do
    type :ticket

    queries do
      # create a field called `list_tickets` that uses the `read` read action to fetch a list of tickets
      list :list_tickets, :read 
    end
  end

The second argument to list :list_tickets, :read is the action that will be called when the query is run. In the current example, the action is :read, which is the generic Read action. Let's create a custom action in order to define query parameters for the listTickets query.

We'll call this action :query_tickets:

  actions do
    defaults [:read, :update, :destroy]
    
    read :query_tickets do
      argument :representative_id, :uuid

      filter(
        expr do
          is_nil(^arg(:representative_id)) or representative_id == ^arg(:representative_id)
        end
      )
    end
  end

  graphql do
    type :ticket

    queries do
      # create a field called `list_tickets` that uses the `:query_tickets` read action to fetch a list of tickets
      list :list_tickets, :query_tickets
    end
  end

In the graphql section, the list/2 call has been changed, replacing the :read action with :query_tickets.

The GraphQL request would look something like this:

query($representative_id: ID) {
  list_tickets(representative_id: $representative_id) {
    id
    representative_id
    subject
  }
}

Mutations and Enums

Now, let's look at how to create a ticket by using a GraphQL mutation.

Let's say you have a Resource that defines an enum-like attribute:

defmodule Helpdesk.Support.Ticket do
  use Ash.Resource,
    ...,
    extensions: [
      AshGraphql.Resource
    ]


  attributes do
    uuid_primary_key :id
    attribute :subject, :string
    attribute :status, :atom, constraints: [one_of: [:open, :closed]]
  end

  actions do
    defaults [:create, :read, :update, :destroy]
  end

  graphql do
    type :ticket

    queries do
      get :get_ticket, :read 
    end
    
    mutations do
      create :create_ticket, :create
    end
  end
end

Above, the following changes have been added:

  1. In the attributes section, the :status attribute has been added.
  2. In the actions section, the :create action has been added.
  3. The :create_ticket mutation has been defined in the new graphql.mutations section.

The :status attribute is an enum that is constrained to the values [:open, :closed]. When used in conjunction with AshGraphql, a GraphQL enum type called TicketStatus will be generated for this attribute. The possible GraphQL values for TicketStatus are OPEN and CLOSED. See Use Enums with GraphQL for more information.

We can now create a ticket with the createTicket mutation:

mutation($input: CreateTicketInput!) {
  createTicket(input: $input) {
    result {
      id
      subject
      status
    }
    errors {
      code
      fields
      message
      shortMessage
      vars
    }
  }
}

Note

  • The resulting ticket data is wrapped in AshGraphql's result object.

  • Validation errors are wrapped in a list of error objects under errors, also specified in the query. AshGraphql does this by default instead of exposing errors in GraphQL's standard errors array. This behavior can be changed by setting root_level_errors? true in the graphql section of your Ash domain module:

    defmodule Helpdesk.Support do
      use Ash.Domain, extensions: [AshGraphql.Domain]
    
      graphql do
        root_level_errors? true
      end
    end
    

If we were to run this mutation in a test, it would look something like this:

input = %{
  subject: "My Ticket",
  status: "OPEN"
}

resp_body =
  post(conn, "/api/graphql", %{
    query: query,
    variables: %{input: input}
  })
  |> json_response(200)

Notice that the status attribute is set to "OPEN" and not "open". It is important that the value of the status be uppercase. This is required by GraphQL enums. AshGraphql will automatically convert the value to the correct case.

The response will look something like this:

{
  "data": {
    "createTicket": {
      "result": {
        "id": "b771e433-0979-4d07-a280-4d12373849aa",
        "subject": "My Ticket",
        "status": "OPEN"
      }
    }
  }
}

Again, AshGraphql will automatically convert the status value from :open to "OPEN".

More GraphQL Docs

If you haven't already, please turn on the documentation tag for AshGraphql. Tags can be controlled at the top of the left navigation menu, under "Including Libraries:".