ash_graphql/documentation/topics/graphql-generation.md

305 lines
6.9 KiB
Markdown
Raw Normal View History

2023-02-21 05:52:47 +13:00
# GraphQL Query Generation
2024-05-02 07:59:26 +12:00
Following where we left off from [Getting Started with GraphQL](/documentation/tutorials/getting-started-with-graphql.md), this guide explores what the GraphQL requests and responses look like for different queries defined with the AshGraphql DSL.
2023-02-21 05:52:47 +13:00
2024-05-02 07:59:26 +12:00
## Fetch Data by ID
2023-02-21 05:52:47 +13:00
```elixir
defmodule Helpdesk.Support.Ticket do
2023-02-21 05:52:47 +13:00
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
2024-05-02 07:59:26 +12:00
get :get_ticket, :read
2023-02-21 05:52:47 +13:00
end
end
end
```
For the `get_ticket` query defined above, the corresponding GraphQL would look like this:
```graphql
2024-05-02 07:59:26 +12:00
query ($id: ID!) {
getTicket(id: $id) {
2023-02-21 05:52:47 +13:00
id
subject
}
}
```
And the response would look similar to this:
```json
{
"data": {
"getTicket": {
2023-02-21 05:52:47 +13:00
"id": "",
"subject": ""
}
}
}
```
Let's look at an example of querying a list of things.
```elixir
graphql do
type :ticket
queries do
# create a field called `get_ticket` that uses the `read` read action to fetch a single ticket
2024-05-02 07:59:26 +12:00
get :get_ticket, :read
2023-02-21 05:52:47 +13:00
# create a field called `list_tickets` that uses the `read` read action to fetch a list of tickets
2024-05-02 07:59:26 +12:00
list :list_tickets, :read
2023-02-21 05:52:47 +13:00
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:
```graphql
query {
listTickets {
2023-02-21 05:52:47 +13:00
id
subject
}
}
```
And the response would look similar to this:
```json
{
"data": {
"listTickets": [
2023-02-21 05:52:47 +13:00
{
"id": "",
"subject": ""
}
]
}
}
```
## Filter Data With Arguments
Now, let's say we want to add query parameters to `listTickets`. How do we do that?
2023-02-21 05:52:47 +13:00
Consider `list :list_tickets, :read` and the `actions` section:
```elixir
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
2024-05-02 07:59:26 +12:00
list :list_tickets, :read
2023-02-21 05:52:47 +13:00
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.
2023-02-21 05:52:47 +13:00
We'll call this action `:query_tickets`:
```elixir
actions do
defaults [:read, :update, :destroy]
2024-05-02 07:59:26 +12:00
2023-02-21 05:52:47 +13:00
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:
```graphql
2024-05-02 07:59:26 +12:00
query ($representative_id: ID) {
2023-02-21 05:52:47 +13:00
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:
```elixir
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
2024-05-02 07:59:26 +12:00
get :get_ticket, :read
end
2024-05-02 07:59:26 +12:00
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](/documentation/guides/use-enums-with-graphql.md) for more information.
We can now create a ticket with the `createTicket` mutation:
```graphql
2024-05-02 07:59:26 +12:00
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
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-02 07:03:06 +13:00
of your Ash domain module:
```elixir
defmodule Helpdesk.Support do
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-02 07:03:06 +13:00
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:
```elixir
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:
2024-05-02 07:59:26 +12:00
```json
{
"data": {
"createTicket": {
"result": {
"id": "b771e433-0979-4d07-a280-4d12373849aa",
"subject": "My Ticket",
"status": "OPEN"
}
}
}
2024-05-02 07:59:26 +12:00
}
```
Again, AshGraphql will automatically convert the `status` value from `:open` to `"OPEN"`.
2023-02-21 05:52:47 +13:00
## 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:".
- [Getting Started With GraphQL](/documentation/tutorials/getting-started-with-graphql.md)
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-02 07:03:06 +13:00
- `AshGraphql.Domain`