defmodule GraphQLDocument do
@moduledoc """
Builds [GraphQL](https://graphql.org/)
[Documents](http://spec.graphql.org/October2021/#sec-Document) from simple Elixir
data structures.
These functions take Elixir data in the same structure as GraphQL and return the analogous GraphQL Document as a `String`.
- `GraphQLDocument.query/2`
- `GraphQLDocument.mutation/2`
- `GraphQLDocument.subscription/2`
Using these abilities, developers can generate GraphQL queries programmatically.
`GraphQLDocument` can be used to create higher-level
[DSL](https://en.wikipedia.org/wiki/Domain-specific_language)s
for writing GraphQL queries.
## Getting Started
> #### Elixir & GraphQL Code Snippets {: .info}
>
> Each Elixir code snippet is immediately followed by the GraphQL that it
> will produce.
All functions called in the code snippets below are in `GraphQLDocument`.
(`import GraphQLDocument` to directly use them.)
### Object Fields
To request a list of fields in an object, include them in a list.
```
query([
human: [
:name,
:height
]
])
```
```gql
query {
human {
name
height
}
}
```
### Arguments
Wrap arguments along with child fields in a tuple.
```
{args, fields}
```
```
query(
human: {[id: "1000"], [
:name,
:height
]}
)
```
```gql
query {
human(id: "1000") {
name
height
}
}
```
#### Argument types and Enums
Elixir primitives (numbers, strings, lists, booleans, etc.) are translated
into the analogous GraphQL primitive.
Enums are expressed with atoms, like `MY_ENUM`, `:MY_ENUM`, or `:"MY_ENUM"`
```
query(
human: {[id: "1000"], [
:name,
height: {[unit: FOOT], []}
]}
)
```
```gql
query {
human(id: "1000") {
name
height(unit: FOOT)
}
}
```
> #### Expressing Arguments Without Sub-fields {: .tip}
>
> Notice the slightly complicated syntax above: `height: {[unit: FOOT], []}`
>
> Since `args` can be expressed in `{args, fields}` tuple, we put `[]` where
> the sub-fields go because there are no sub-fields.
>
> This can also be expressed as `height: field(args: [unit: FOOT])`. See `field/1`.
### Mixing Lists and Keyword Lists
Since GraphQL supports a theoretically infinite amount of nesting, you can also
nest as much as needed in the Elixir structure.
Furthermore, we can take advantage of Elixir's syntax feature that allows a
regular list to be "mixed" with a keyword list. (The keyword pairs must be at
the end.)
```
# Elixir allows lists with a Keyword List as the final members
[
:name,
:height,
friends: [
:name,
:age
]
]
```
Using this syntax, we can build a nested structure where we select primitive
fields (like `:name` below) alongside object fields (like `:friends`).
```
query(
human: {[id: "1000"], [
:name,
:height,
friends: {[olderThan: 30], [
:name,
:height
]}
]}
)
```
```gql
query {
human(id: "1000") {
name
height
friends(olderThan: 30) {
name
height
}
}
}
```
### Variables
To express a variable as a value, use `{:var, var_name}` syntax or the
`var/1` function.
The variable definition is passed as an option to `query/2`, `mutation/2`, or
`subscription/2`.
Variable types can take the form `Int`, `{Int, null: false}`, or `{Int, default: 1}`.
```
query(
[
user: {
[id: var(:myId)],
[
:name,
friends: {
[type: {:var, :friendType}],
[:name]
}
]
}
],
variables: [
myId: {Int, null: false},
friendType: {String, default: "best"}
]
)
```
```gql
query ($myId: Int!, $friendType: String = "best") {
user(id: $myId) {
name
friends(type: $friendType) {
name
}
}
}
```
### Fragments
To express a fragment, use the `:...` atom as the field name, similar to how
you would in GraphQL.
The fragment definition is passed as an option to `query/2`, `mutation/2`, or
`subscription/2`.
Inline fragments and fragment definitions use the `on/1` function to specify
the type condition.
```
query(
[
self: [
...: {
on(User),
[skip: [if: true]],
[:password, :passwordHash]
},
friends: [
...: :friendFields
]
]
],
fragments: [
friendFields: {on(User), [
:id,
:name,
profilePic: field(args: [size: 50])
]}
]
)
```
```gql
query {
self {
... on User @skip(if: true) {
password
passwordHash
}
friends(first: 10) {
...friendFields
}
}
}
fragment friendFields on User {
id
name
profilePic(size: 50)
}
```
## Features That Require `field()`
The `field/1` and `field/2` functions are required in order to express [Aliases](#module-aliases)
and [Directives](#module-directives).
### Aliases
Express an alias by putting the alias in place of the field name, and pass
the field name as the first argument to `field/2`.
In the example below, `me` is the alias and `user` is the field.
> #### Spot the Keyword List {: .info}
>
> `args:` and `select:` below are members of an "invisible" keyword list
> using Elixir's [call syntax](https://hexdocs.pm/elixir/Keyword.html#module-call-syntax).
```
query(
me: field(
:user,
args: [id: 100],
select: [:name, :email]
)
)
```
```gql
query {
me: user(id: 100) {
name
email
}
}
```
### Directives
Express a directive by passing `directives:` to `field/1` or `field/2`.
A directive can be a single name (as an atom or string) or a tuple in
`{name, args}` format.
```
query(
self: field(
directives: [:debug, log: [level: "warn"]]
select: [:name, :email]
)
)
```
```gql
query {
self @debug @log(level: "warn") {
name
email
}
}
```
## Not-yet-supported features
`GraphQLDocument` does not currently have the ability to generate Type System
definitions, although they technically belong in a Document.
"""
alias GraphQLDocument.{Field, Fragment, Name, Operation, Selection}
@type field_config :: [
{:args, [Argument.t()]}
| {:directives, [Directive.t()]}
| {:select, [Selection.t()]}
]
@doc """
If you want to express a field with directives or an alias, you must use this
function.
See `field/2` if you want to specify an alias.
### Examples
iex> field(
...> args: [id: 2],
...> directives: [:debug],
...> select: [:name]
...> )
{
:field,
[
args: [id: 2],
directives: [:debug],
select: [:name]
]
}
"""
@spec field(field_config) :: Field.spec()
def field(config), do: {:field, config}
@doc """
If you want to express a field with an alias, you must use this function.
Put the alias where you would normally put the field name, and pass the
field name as the first argument to `field/2`.
See `field/1` if you want to specify directives without an alias.
### Examples
iex> field(
...> :user,
...> args: [id: 2],
...> directives: [:debug],
...> select: [:name]
...> )
{
:field,
:user,
[
args: [id: 2],
directives: [:debug],
select: [:name]
]
}
"""
@spec field(Name.t(), field_config) :: Field.spec()
def field(name, config), do: {:field, name, config}
@doc """
Wraps a variable name in a `GraphQLDocument`-friendly tuple.
### Example
iex> var(:foo)
{:var, :foo}
"""
def var(name) when is_binary(name) or is_atom(name), do: {:var, name}
@doc """
Creates a [TypeCondition](http://spec.graphql.org/October2021/#TypeCondition)
for a [Fragment](http://spec.graphql.org/October2021/#sec-Language.Fragments).
See `GraphQLDocument.Fragment` for more details.
### Example
iex> on(User)
{:on, User}
"""
@spec on(Name.t()) :: Fragment.type_condition()
def on(name) when is_binary(name) or is_atom(name), do: {:on, name}
@doc ~S'''
Generate a GraphQL query document.
See the **[Getting Started](#module-getting-started)** section for more details.
### Example
iex> query(
...> [
...> customer: {[id: var(:customerId)], [
...> :name,
...> :email,
...> phoneNumbers: field(args: [type: MOBILE]),
...> cartItems: [
...> :costPerItem,
...> ...: :cartDetails
...> ]
...> ]}
...> ],
...> variables: [customerId: Int],
...> fragments: [cartDetails: {
...> on(CartItem),
...> [:sku, :description, :count]
...> }]
...> )
"""
query ($customerId: Int) {
customer(id: $customerId) {
name
email
phoneNumbers(type: MOBILE)
cartItems {
costPerItem
...cartDetails
}
}
}
\nfragment cartDetails on CartItem {
sku
description
count
}\
"""
'''
@spec query([Selection.t()], [Operation.option()]) :: String.t()
def query(selections, opts \\ []) do
Operation.render(:query, selections, opts)
end
@doc ~S'''
Generate a GraphQL mutation document.
See the **[Getting Started](#module-getting-started)** section for more details.
### Example
iex> mutation(
...> registerUser: {
...> [
...> name: "Ben",
...> hexUsername: "benwilson512",
...> packages: ["absinthe", "ex_aws"]
...> ],
...> [
...> :id,
...> ]
...> }
...> )
"""
mutation {
registerUser(name: "Ben", hexUsername: "benwilson512", packages: ["absinthe", "ex_aws"]) {
id
}
}\
"""
'''
@spec mutation([Selection.t()], [Operation.option()]) :: String.t()
def mutation(selections, opts \\ []) do
Operation.render(:mutation, selections, opts)
end
@doc """
Generate a GraphQL subscription document.
Works like `query/2` and `mutation/2`, except that it generates a
subscription.
See the **[Getting Started](#module-getting-started)** section for more details.
"""
@spec subscription([Selection.t()], [Operation.option()]) :: String.t()
def subscription(selections, opts \\ []) do
Operation.render(:subscription, selections, opts)
end
end