defmodule Absinthe.Federation.Notation do @moduledoc """ Module that includes macros for annotating a schema with federation directives. ## Example defmodule MyApp.MySchema.Types do use Absinthe.Schema.Notation + use Absinthe.Federation.Notation end """ defmacro __using__(_opts) do notations() end @spec notations() :: Macro.t() defp notations() do quote do import Absinthe.Federation.Notation, only: :macros end end @doc """ Adds a `@key` directive to the type which indicates a combination of fields that can be used to uniquely identify and fetch an object or interface. This allows the type to be extended by other services. A string rather than atom is used here to support composite keys e.g. `id organization { id }` ## Example object :user do key_fields("id") field :id, non_null(:id) end ## SDL Output type User @key(fields: "id") { id: ID! } """ defmacro key_fields(fields) when is_binary(fields) or is_list(fields) do quote do meta :key_fields, unquote(fields) end end @doc """ Adds the `@external` directive to the field which marks a field as owned by another service. This allows service A to use fields from service B while also knowing at runtime the types of that field. ## Example object :user do extends() key_fields("email") field :email, :string do external() end field :reviews, list_of(:review) end ## SDL Output # extended from the Users service type User @key(fields: "email") @extends { email: String @external reviews: [Review] } This type extension in the Reviews service extends the User type from the Users service. It extends it for the purpose of adding a new field called reviews, which returns a list of `Review`s. """ defmacro external() do quote do meta :external, true end end @doc """ Adds the `@requires` directive which is used to annotate the required input fieldset from a base type for a resolver. It is used to develop a query plan where the required fields may not be needed by the client, but the service may need additional information from other services. ## Example object :user do extends() key_fields("id") field :id, non_null(:id) do external() end field :email, :string do external() end field :reviews, list_of(:review) do requires_fields("email") end end ## SDL Output # extended from the Users service type User @key(fields: "id") @extends { id: ID! @external email: String @external reviews: [Review] @requires(fields: "email") } In this case, the Reviews service adds new capabilities to the `User` type by providing a list of `reviews` related to a `User`. In order to fetch these `reviews`, the Reviews service needs to know the `email` of the `User` from the Users service in order to look up the `reviews`. This means the `reviews` field / resolver requires the `email` field from the base `User` type. """ defmacro requires_fields(fields) when is_binary(fields) do quote do meta :requires_fields, unquote(fields) end end @doc """ Adds the `@provides` directive which is used to annotate the expected returned fieldset from a field on a base type that is guaranteed to be selectable by the gateway. ## Example object :review do key_fields("id") field :id, non_null(:id) field :product, :product do provides_fields("name") end end object :product do extends() key_fields("upc") field :upc, :string do external() end field :name, :string do external() end end ## SDL Output type Review @key(fields: "id") { product: Product @provides(fields: "name") } type Product @key(fields: "upc") @extends { upc: String @external name: String @external } When fetching `Review.product` from the Reviews service, it is possible to request the `name` with the expectation that the Reviews service can provide it when going from review to product. `Product.name` is an external field on an external type which is why the local type extension of `Product` and annotation of `name` is required. """ defmacro provides_fields(fields) when is_binary(fields) do quote do meta :provides_fields, unquote(fields) end end @doc """ Adds the `@extends` directive to the type to indicate that the type as owned by another service. ## Example object :user do extends() key_fields("id") field :id, non_null(:id) end ## SDL Output type User @key(fields: "id") @extends { id: ID! } """ defmacro extends() do quote do meta :extends, true end end @doc """ Adds the `@shareable` directive to the type to indicate that a field can be resolved by multiple subgraphs. ## Example object :user do key_fields("id") shareable() field :id, non_null(:id) end ## SDL Output type User @key(fields: "id") @shareable { id: ID! } """ defmacro shareable() do quote do meta :shareable, true end end @doc """ The `@override` directive is used to indicate that the current subgraph is taking responsibility for resolving the marked field away from the subgraph specified in the from argument. The progressive `@override` feature enables the gradual, progressive deployment of a subgraph with an `@override` field. As a subgraph developer, you can customize the percentage of traffic that the overriding and overridden subgraphs each resolve for a field. ## Example # @override object :user do key_fields("id") field :id, non_null(:id) field :name, :string do override_from("SubgraphA") end end # progressive @override object :user do key_fields("id") field :id, non_null(:id) field :name, :string do override_from("SubgraphA", label: "percent(10)") end end ## SDL Output # @override type User @key(fields: "id") { id: ID! name: String @override(from: "SubgraphA") } # progressive @override type User @key(fields: "id") { id: ID! name: String @override(from: "SubgraphA", label: "percent(10)") } """ defmacro override_from(subgraph, opts \\ []) when is_binary(subgraph) and is_list(opts) do label = Keyword.get(opts, :label) if label do quote do meta :override_from, from: unquote(subgraph), label: unquote(label) end else quote do meta :override_from, from: unquote(subgraph) end end end @doc """ The `@inaccessible` directive indicates that a field or type should be omitted from the gateway's API schema, even if it's also defined in other subgraphs. ## Example object :user do key_fields("id") field :id, non_null(:id) field :name, :string do inaccessible() end end ## SDL Output type User @key(fields: "id") { id: ID! name: String @inaccessible } """ defmacro inaccessible() do quote do meta :inaccessible, true end end @doc """ Adds the `@interfaceObject` directive to the field which indicates that the object definition serves as an abstraction of another subgraph's entity interface. This abstraction enables a subgraph to automatically contribute fields to all entities that implement a particular entity interface. During composition, the fields of every `@interfaceObject` are added both to their corresponding interface definition and to all entity types that implement that interface. More information can be found on: https://www.apollographql.com/docs/federation/federated-types/interfaces ## Example object :media do key_fields("id") interface_object() field :id, non_null(:id), do: external() field :reviews, non_null(list_of(non_null(:review))) end object :review do field :score, non_null(:integer) end ## SDL Output type Media @interfaceObject @key(fields: "id") { id: ID! @external reviews: [Review!]! } type Review { score: Int! } """ defmacro interface_object() do quote do meta :interface_object, true end end @doc """ The `@tag` directive indicates whether to include or exclude the field/type from your contract schema. ## Example object :user do key_fields("id") field :id, non_null(:id) field :ssn, :string do tag("internal") end end ## SDL Output type User @key(fields: "id") { id: ID! name: String @tag(name: "internal") } """ defmacro tag(name) when is_binary(name) do quote do meta :tag, unquote(name) end end @doc """ The `@authenticated` directive marks specific fields and types as requiring authentication. ## Example object :secret do field :text, :string do authenticated() end end ## SDL Output type Secret { text: String @authenticated } """ defmacro authenticated() do quote do meta :authenticated, true end end @doc """ The `@requiresScopes` directive marks fields and types as restricted based on required scopes. ## Example object :user do field :id, non_null(:id) field :username, :string field :email, :string do requires_scopes([["read:email"]]) end field :profile_image, :string end ## SDL Output type User { id: ID! username: String email: String @requiresScopes(scopes: [["read:email"]]) profileImage: String } """ defmacro requires_scopes(scopes) when is_list(scopes) do quote do meta :requires_scopes, unquote(scopes) end end @doc """ The `@policy` directive marks fields and types as restricted based on authorization policies evaluated in a Rhai script or coprocessor. This enables custom authorization validation beyond authentication and scopes. ## Example object :user do field :id, non_null(:id) field :username, :string field :email, :string field :profile_image, :string field :credit_card, :string do policy([["read_credit_card"]]) end end ## SDL Output type User { id: ID! username: String email: String profileImage: String credit_card: String @policy(policies: [["read_credit_card"]]) } """ defmacro policy(policies) when is_list(policies) do quote do meta :policies, unquote(policies) end end @doc """ The `@listSize` directive is used to customize the cost calculation of the demand control feature of GraphOS Router. ## Example object :post do field :text, :string end object :user do field :posts, non_null(list_of(:post)) do list_size( assumed_size: 10, slicing_arguments: ["first", "last"], sized_fields: ["postCount"], required_one_slicing_argument: false ) arg :first, :integer arg :last, :integer end end ## SDL Output type User { posts(first: Int, last: Int): [Post]! @listSize(assumedSize: 10, slicingArguments: ["first", "last"], sizedFields: ["postCount"], requiredOneSlicingArgument: false) } """ defmacro list_size(opts) when is_list(opts) do quote do meta :list_size, unquote(opts) end end @doc """ The `@cost` directive defines a custom weight for a schema location. For GraphOS Router, it customizes the operation cost calculation of the demand control feature. ## Example object :user do field :posts, list_of(:post) do cost(5) resolve &PostResolvers.list_for_user/3 end end ## SDL Output type User { posts: [Post] @cost(weight: 5) } """ defmacro cost(weight) when is_integer(weight) do quote do meta :cost, unquote(weight) end end @doc """ The `@context` directive defines a named context from which a field of the annotated type can be passed to a receiver of the context. The receiver must be a field annotated with the `@fromContext` directive. ## Example object :user do context("userContext") field :id, non_null(:id) field :name, :string end ## SDL Output type User @context(name: "userContext") { id: ID! name: String } """ defmacro context(name) when is_binary(name) do quote do meta :context, unquote(name) end end @doc """ The `@link` directive links definitions from an external specification to this schema. Every Federation 2 subgraph uses the `@link` directive to import the other federation-specific directives. **NOTE:** If you're using Absinthe v1.7.1 or later, instead of using this macro, it's preferred to use the `extend schema` method you can find in the [README](README.md#federation-v2). ## Example link(url: "https://specs.apollo.dev/federation/v2.0", import: ["@key", "@tag", "@shareable"]) query do field :me, :user end object :user do key_fields("id") shareable() field :id, non_null(:id) field :ssn, :string do tag("internal") end end ## SDL Output schema @link(url: \"url: https:\\/\\/specs.apollo.dev\\/federation\\/v2.0\", import: ["@key", "@tag", "@shareable"]) type User @key(fields: "id") @shareable { id: ID! name: String @tag(name: "internal") } """ defmacro link(opts) when is_list(opts) do quote do opts = unquote(opts) query_type = Keyword.get(opts, :query_type_name, "RootQueryType") mutation_type = Keyword.get(opts, :mutation_type_name, "RootMutationType") url_arg = opts |> Keyword.fetch!(:url) |> (&~s(url: \"#{&1}\")).() import_arg = opts |> Keyword.fetch!(:import) |> Enum.map(fn arg when is_binary(arg) -> ~s("#{arg}") %{name: name, as: renamed_as} -> ~s({ name: "#{name}", as: "#{renamed_as}" }) end) |> (&", import: [#{&1}]").() namespace_arg = case Keyword.get(opts, :as) do namespace when is_nil(namespace) -> "" namespace when is_binary(namespace) -> ~s(, as: "#{namespace}") end args = "#{url_arg}#{import_arg}#{namespace_arg}" import_sdl """ schema @link(#{args}) { query: #{query_type} mutation: #{mutation_type} } """ end end end