Absinthe GraphQL for Elixir

repository·main·Indexed 26 days ago

https://github.com/absinthe-graphql/absinthe

A complete GraphQL implementation for Elixir providing compile-time schema verification and advanced resolution capabilities. Features include built-in batching to avoid N+1 queries, integration with the dataloader package via Absinthe.Middleware.Dataloader, and flexible document adapters like LanguageConventions to bridge Elixir snake_case with camelCase. Includes guides for connecting Apollo Client via HTTP and websockets for subscriptions.

Tokens
23.3K
Snippets
86
Records
115
Agent score
88%

What's inside Absinthe

  1. Overview of Absinthe GraphQL toolkit

    main

    Absinthe is a GraphQL toolkit for Elixir that implements the GraphQL specification. It is designed to be idiomatic to Elixir, utilizing declarative macros for schema definition and a flexible middleware/plugin system for resolution.

    Absinthe functionality is divided into two main areas:

    1. Defining Schemas: Using macros to define data entities, relationships, queries, mutations, subscriptions, custom scalars, and directives. It also allows defining resolution functions to access data.
    2. Executing Documents: Handling standard GraphQL queries, mutations, and subscriptions. This includes support for variables, complexity analysis for safety, context integration (for authentication/authorization), introspection, and multipart file uploads (via absinthe_plug).
  2. Absinthe Integrations

    main

    Absinthe integrates with various backend and frontend technologies:

    Backend (Elixir)

    • HTTP APIs: Use absinthe_plug and absinthe_phoenix to support Plug and Phoenix.
    • Database/Data Loading: Use the dataloader package for Ecto integration to solve N+1 query problems.

    Frontend (JavaScript)

    • Clients: Supports Relay and Apollo Client.
    • Subscriptions: Supports channel-based subscriptions via absinthe-socket.
  3. Approaches to testing Absinthe

    main

    There are three primary ways to test your Absinthe GraphQL implementation:

    1. Resolver functions: Test individual resolver functions directly. This is useful for unit testing the core logic.
    2. GraphQL document execution: Use Absinthe.run/3 to execute GraphQL documents directly. This tests the schema and execution engine without the HTTP layer.
    3. Full HTTP cycle (Recommended): Use absinthe_plug to test the complete request/response cycle via HTTP. This is the most comprehensive method as it exercises the transport layer and integration with your web server (e.g., Phoenix).
  4. Return errors from Absinthe resolvers

    main

    In Absinthe, you can return one or more errors for a field by returning the {:error, error_value} tuple from your resolver.

    The error_value is flexible and can be any of the following:

    • A simple string.
    • A map containing a :message key (plus any additional serializable metadata).
    • A keyword list containing a :message key (plus any additional serializable metadata).
    • A list containing multiple values of any of the types above.
    • Any value compatible with to_string/1 (e.g., atoms or numbers) for generic error handling.
  5. Handle breaking changes in Absinthe v1.5

    main

    Upgrading to v1.5 introduces several breaking changes related to how values and types are validated. Ensure your schema and subscription implementations account for the following:

    1. Compile-time Default Values: Default values in your schema are now evaluated at compile time. Using functions like DateTime.utc_now() in a default_value option will fix the value to the time the module was compiled, rather than the time the query is executed.
    2. Scalar Output Validation: Scalar outputs are now strictly type-checked. If a resolver returns a data type that does not match the defined scalar type, an exception will be raised.
    3. Variable Type Validation: Variable types must align exactly with the argument type. Absinthe no longer allows variables of different types to be used if they happen to parse successfully.
    4. Field Name Validation: Field names are now validated against the GraphQL specification. If you need to revert to the previous behavior (allowing invalid field names), remove Absinthe.Phase.Schema.Validation.NamesMustBeValid from your schema pipeline.
  6. Implement Dataloader in an Absinthe Schema

    main

    To integrate Dataloader into your Absinthe schema, you must perform three steps: define a data source, register it in the context, and enable the middleware plugin.

    1. Data Source: Create a module (often your context) that provides a data/0 function returning a Dataloader.Ecto source. 2. Context Setup: In your schema's context/1 function, initialize Dataloader.new(), add your source using Dataloader.add_source/2, and return the loader in the context map under the :loader key. 3. Plugin Registration: Add Absinthe.Middleware.Dataloader to the list returned by your schema's plugins/0 function.

    # Example Schema configuration
    # Inside your Schema module
    
    def context(ctx) do
      loader = 
        Dataloader.new()
        |> Dataloader.add_source(Blog, Blog.data())
    
      Map.put(ctx, :loader, loader)
    end
    
    def plugins do
      [Absinthe.Middleware.Dataloader] ++ Absinthe.Plugin.defaults()
    end
  7. Integrate Dataloader with Absinthe

    main

    To prevent N+1 query problems and simplify association loading, you can integrate the dataloader Elixir library with Absinthe. This allows you to use the dataloader(Context) resolver syntax in your schema definitions.

    1. Add Dependency

    Add dataloader to your mix.exs file:

    2. Configure the Context and Plugins

    In your Absinthe.Schema module, you must:

    1. Add Absinthe.Middleware.Dataloader to the plugins/0 list.
    2. Initialize a new Dataloader in the context/1 function and add your data source to it.

    3. Define the Data Source

    Your context module (e.g., Content) should provide a function that returns a Dataloader.Ecto instance (or similar) configured with your Repo and a query function.

    # 1. mix.exs
    defp deps do
      [
        {:dataloader, "~> 1.0.7"}
      ]
    end
    
    # 2. lib/blog/content.ex
    def data(), do: Dataloader.Ecto.new(Repo, query: &query/2)
    
    def query(queryable, params) do
      queryable
    end
    
    # 3. lib/blog_web/schema.ex
    defmodule BlogWeb.Schema do
      use Absinthe.Schema
    
      def context(ctx) do
         loader = 
           Dataloader.new()
           |> Dataloader.add_source(Content, Content.data())
    
        Map.put(ctx, :loader, loader)
      end
    
      def plugins do
        [Absinthe.Middleware.Dataloader | Absinthe.Plugin.defaults()]
      end
    end
  8. Absinthe educational resources

    main

    To learn Absinthe, you can use the following resources:

    Books

    • Craft GraphQL APIs in Elixir with Absinthe by the creators of Absinthe (Prag Program).

    Online Resources

    Videos

    General GraphQL Knowledge

  9. Configure Apollo Client with an HTTP link

    main

    To connect Apollo Client to an Absinthe server via HTTP, use createHttpLink from @apollo/client. No special Absinthe configuration is required for basic HTTP usage. If you need to include authentication headers (e.g., an Authorization bearer token from a cookie), use the setContext helper to create an authLink and chain it with your httpLink using .concat().

    import { ApolloClient, InMemoryCache, createHttpLink } from "@apollo/client";
    import { setContext } from "@apollo/client/link/context";
    import Cookies from "js-cookie";
    
    // Create an HTTP link to the Absinthe server.
    const httpLink = createHttpLink({
      uri: "http://localhost:4000/graphql"
    });
    
    // Use setContext to create a chainable link object that sets
    // the token cookie to the Authorization header.
    const authLink = setContext((_, { headers }) => {
      // Get the authentication token from the cookie if it exists.
      const token = Cookies.get("token");
    
      // Add the new Authorization header.
      return {
        headers: {
          ...headers,
          authorization: token ? `Bearer ${token}` : ""
        }
      };
    });
    
    // Chain the HTTP link and the authorization link.
    const link = authLink.concat(httpLink);
    
    const cache = new InMemoryCache();
    
    const client = new ApolloClient({
      link,
      cache
    });
  10. Setup Absinthe.Plug in a Phoenix Endpoint

    main

    If your entire API is GraphQL-based, you can plug Absinthe.Plug directly into your MyApp.Endpoint and remove the router.

    defmodule MyApp.Endpoint do
      use Phoenix.Endpoint, otp_app: :my_app
    
      plug Plug.RequestId
      plug Plug.Logger
    
      plug Plug.Parsers,
        parsers: [:urlencoded, :multipart, :json],
        pass: ["*/*"],
        json_decoder: Jason
    
      plug Absinthe.Plug,
        schema: MyAppWeb.Schema
    end
  11. Reconnect the websocket link on auth changes

    main
    To refresh subscriptions after a user logs in or out, you can force a reconnection by calling phoenixSocket.conn.close();. The Phoenix socket will detect the closed connection and automatically attempt to reconnect. Because you provided a function to the params option in the PhoenixSocket constructor, the new connection will use the updated authentication state.
  12. Implement a mutation resolver with context-based authorization

    main

    Mutation resolvers in Absinthe typically accept three arguments: parent, args, and resolution. The resolution argument (an Absinthe.Resolution struct) contains the context, which is the integration point for external data like authenticated users (often provided by a Plug).

    To implement authorization, pattern match on the context within your resolver to ensure the required user or data is present before proceeding with the business logic.

    def create_post(_parent, args, %{context: %{current_user: user}}) do
      Blog.Content.create_post(user, args)
    end
    
    def create_post(_parent, _args, _resolution) do
      {:error, "Access denied"}
    end