defmodule Absinthe.Relay.Mutation.Notation.Classic do @moduledoc """ Support for Relay Classic mutations with single inputs and client mutation IDs. The `payload` macro can be used by schema designers to support mutation fields that receive a single input object argument with a client mutation ID and return that ID as part of the response payload. More information can be found at https://facebook.github.io/relay/docs/guides-mutations.html ## Example In this example we add a mutation field `:simple_mutation` that accepts an `input` argument (which is defined for us automatically) which contains an `:input_data` field. We also declare the output will contain a field, `:result`. Notice the `resolve` function doesn't need to know anything about the wrapping `input` argument -- it only concerns itself with the contents -- and the client mutation ID doesn't need to be dealt with, either. It will be returned as part of the response payload automatically. ``` mutation do payload field :simple_mutation do input do field :input_data, non_null(:integer) end output do field :result, :integer end resolve fn %{input_data: input_data}, _ -> # Some mutation side-effect here {:ok, %{result: input_data * 2}} end end end ``` Here's a query document that would hit this field: ```graphql mutation DoSomethingSimple { simpleMutation(input: {inputData: 2, clientMutationId: "abc"}) { result clientMutationId } } ``` And here's the response: ```json { "data": { "simpleMutation": { "result": 4, "clientMutationId": "abc" } } } ``` Note the above code would create the following types in our schema, ad hoc: - `SimpleMutationInput` - `SimpleMutationPayload` For this reason, the identifier passed to `payload field` must be unique across your schema. ## The Escape Hatch The mutation macros defined here are just for convenience; if you want something that goes against these restrictions, don't worry! You can always just define your types and fields using normal (`field`, `arg`, `input_object`, etc) schema notation macros as usual. """ use Absinthe.Schema.Notation alias Absinthe.Blueprint alias Absinthe.Blueprint.Schema alias Absinthe.Relay.Schema.Notation @doc """ Define a mutation with a single input and a client mutation ID. See the module documentation for more information. """ defmacro payload({:field, meta, args}, do: block) do Notation.payload(meta, args, [default_private(), block]) end defmacro payload({:field, meta, args}) do Notation.payload(meta, args, default_private()) end defp default_private() do [ # This indicates to the Relay schema phase that this field should automatically # generate both input and payload types if they are not defined within the field # itself. The `input` notation also autogenerates the `input` argument to the field quote do private(:absinthe_relay, :payload, {:fill, unquote(__MODULE__)}) private(:absinthe_relay, :input, {:fill, unquote(__MODULE__)}) end ] end # # INPUT # @doc """ Defines the input type for your payload field. See the module documentation for an example. """ defmacro input(identifier, do: block) do Notation.input(__MODULE__, identifier, block) end # # PAYLOAD # @doc """ Defines the output (payload) type for your payload field. See the module documentation for an example. """ defmacro output(identifier, do: block) do Notation.output(__MODULE__, identifier, block) end def additional_types(:input, %Schema.FieldDefinition{identifier: field_ident}) do %Schema.InputObjectTypeDefinition{ name: Notation.ident(field_ident, :input) |> Atom.to_string() |> Macro.camelize(), identifier: Notation.ident(field_ident, :input), module: __MODULE__, __private__: [absinthe_relay: [input: {:fill, __MODULE__}]], __reference__: Absinthe.Schema.Notation.build_reference(__ENV__) } end def additional_types(:payload, %Schema.FieldDefinition{identifier: field_ident}) do %Schema.ObjectTypeDefinition{ name: Notation.ident(field_ident, :payload) |> Atom.to_string() |> Macro.camelize(), identifier: Notation.ident(field_ident, :payload), module: __MODULE__, __private__: [absinthe_relay: [payload: {:fill, __MODULE__}]], __reference__: Absinthe.Schema.Notation.build_reference(__ENV__) } end def additional_types(_, _), do: [] def fillout(:input, %Schema.FieldDefinition{} = field) do Absinthe.Relay.Mutation.Notation.Modern.add_input_arg(field) end def fillout(:input, %Schema.InputObjectTypeDefinition{} = input) do # We could add this to the additional_types above, but we also need to fill # out this field if the user specified the types. It's easier to leave it out # of the defaults, and then unconditionally apply it after the fact. %{input | fields: [client_mutation_id_field() | input.fields]} end def fillout(:payload, %Schema.ObjectTypeDefinition{} = payload) do %{payload | fields: [client_mutation_id_field() | payload.fields]} end def fillout(_, node) do node end defp client_mutation_id_field() do %Blueprint.Schema.FieldDefinition{ name: "client_mutation_id", identifier: :client_mutation_id, type: %Blueprint.TypeReference.NonNull{of_type: :string}, module: __MODULE__, __reference__: Absinthe.Schema.Notation.build_reference(__ENV__) } end end