# EctoGraphql
[](https://hex.pm/packages/ecto_graphql)
[](https://hexdocs.pm/ecto_graphql)
[](https://opensource.org/licenses/MIT)
`ecto_graphql` is a **library for deriving Absinthe GraphQL APIs from Ecto schemas**.
It derives:
- GraphQL **object and input types** from Ecto schemas
- **Association fields** with automatic Dataloader resolution
- **Query and mutation** definitions
- **Resolver stubs** ready for your business logic
- Automatic **integration** with your root schema
The goal is to eliminate repetitive boilerplate by deriving your GraphQL API directly from your Ecto schemas.
## Installation
Add the dependency to your `mix.exs`:
```elixir
def deps do
[
{:ecto_graphql, "~> 0.2.0"},
{:dataloader, "~> 2.0"} # Required for association support
]
end
```
Then run:
```bash
mix deps.get
```
## What Gets Generated
Using a single Mix task, EctoGraphql generates:
- **GraphQL types** — object types and input types for mutations
- **Queries** — list all and get by ID
- **Mutations** — create, update, and delete operations
- **Resolvers** — function stubs for you to implement business logic
- **Automatic imports** — seamless integration into your root schema
All generated code is **plain Elixir** that you can modify, extend, or refactor as needed.
## Mix Task
### From Ecto Schema (Recommended)
```bash
mix gql.gen Accounts lib/example/accounts/user.ex
```
This reads the Ecto schema file and automatically:
1. Extracts all schema fields
2. Maps Ecto types to GraphQL types
3. Generates type definitions, queries, mutations, and resolvers
4. Integrates generated modules into your root schema
### Override Schema Name
```bash
mix gql.gen Accounts Person lib/example/accounts/user.ex
```
Use this when your GraphQL schema name should differ from the Ecto table name.
### Manual Field Definition
```bash
mix gql.gen Accounts User name:string email:string age:integer
```
For quick prototyping or when you don't have an Ecto schema yet.
## Generated File Structure
For context `Accounts` and schema `User`, the generator creates:
```
lib/example_web/graphql/accounts/
├── type.ex # GraphQL object and input types
├── schema.ex # Query and mutation definitions
└── resolvers.ex # Resolver function stubs
```
Existing files are updated intelligently without overwriting your custom code.
## Example Ecto Schema
```elixir
defmodule MyApp.Accounts.User do
use Ecto.Schema
schema "users" do
field :name, :string
field :email, :string
field :password_hash, :string
timestamps(type: :utc_datetime)
end
end
```
## Runtime Macros
EctoGraphql provides two powerful macros for defining GraphQL types at compile-time from your Ecto schemas:
- **`gql_object`** - Creates complete object definitions
- **`gql_fields`** - Generates field definitions within existing objects
### Quick Start
```elixir
defmodule MyAppWeb.Schema.Types do
use Absinthe.Schema.Notation
use EctoGraphql
# Complete object definition
gql_object(:user, MyApp.Accounts.User)
# Or use gql_fields within an object
object :product do
gql_fields(MyApp.Catalog.Product)
end
end
```
### Association Support
EctoGraphql automatically detects `has_one`, `has_many`, and `belongs_to` associations and generates fields with Dataloader resolvers:
```elixir
# Given this Ecto schema:
defmodule MyApp.Accounts.User do
use Ecto.Schema
schema "users" do
field :name, :string
has_one :profile, MyApp.Accounts.Profile
has_many :posts, MyApp.Blog.Post
end
end
# This:
gql_object(:user, MyApp.Accounts.User)
# Generates:
object :user do
field :id, :id
field :name, :string
field :profile, :profile, resolve: dataloader(:ecto)
field :posts, list_of(:post), resolve: dataloader(:ecto)
end
```
**Note:** Input objects (`gql_input_object`) automatically exclude associations since they're not valid input types.
#### Dataloader Setup
To use associations, configure Dataloader in your schema:
```elixir
defmodule MyAppWeb.Graphql.Schema do
use Absinthe.Schema
def context(ctx) do
loader =
Dataloader.new()
|> Dataloader.add_source(:ecto, Dataloader.Ecto.new(MyApp.Repo))
Map.put(ctx, :loader, loader)
end
def plugins do
[Absinthe.Middleware.Dataloader] ++ Absinthe.Plugin.defaults()
end
end
```
### `gql_object` - Complete Object Definitions
Use `gql_object` to quickly create a complete GraphQL object from an Ecto schema.
#### Basic Usage
```elixir
# Generate all fields
gql_object(:user, MyApp.Accounts.User)
```
#### Field Filtering
```elixir
# Include only specific fields
gql_object(:user_public, MyApp.Accounts.User, only: [:id, :name, :email])
# Exclude sensitive fields
gql_object(:user, MyApp.Accounts.User, except: [:password_hash, :recovery_token])
```
#### Custom Fields
Add or override fields using a `do` block:
```elixir
gql_object :user, MyApp.Accounts.User do
# Add a custom field
field :full_name, :string do
resolve fn user, _, _ ->
{:ok, "#{user.first_name} #{user.last_name}"}
end
end
# Override an auto-generated field
field :email, :string do
resolve fn user, _, _ ->
if user.email_public, do: {:ok, user.email}, else: {:ok, "[hidden]"}
end
end
end
```
#### Combining Options and Custom Fields
```elixir
gql_object :user, MyApp.Accounts.User, except: [:inserted_at, :updated_at] do
field :member_since, :string do
resolve fn user, _, _ ->
days = DateTime.diff(DateTime.utc_now(), user.inserted_at, :day)
{:ok, "#{days} days"}
end
end
end
```
### `gql_fields` - Field Generation
Use `gql_fields` when you need fine-grained control over your object structure.
#### Basic Usage
```elixir
object :user do
gql_fields(MyApp.Accounts.User)
end
```
#### Mixing with Custom Fields
```elixir
object :user do
gql_fields(MyApp.Accounts.User, except: [:password_hash])
# Add custom fields
field :avatar_url, :string do
resolve fn user, _, _ ->
{:ok, "https://cdn.example.com/avatars/#{user.id}.jpg"}
end
end
field :is_admin, :boolean do
resolve fn user, _, _ ->
{:ok, user.role == :admin}
end
end
end
```
#### Multiple Schemas in One Object
```elixir
object :user_profile do
gql_fields(MyApp.Accounts.User, only: [:id, :name, :email])
gql_fields(MyApp.Accounts.Profile, except: [:user_id, :id])
# Add computed fields
field :display_name, :string
end
```
#### When to Use Each Macro
**Use `gql_object` when:**
- You want a quick, complete object definition
- Most fields map directly from your Ecto schema
- You only need to add a few custom fields
**Use `gql_fields` when:**
- You need precise control over field ordering
- You're combining fields from multiple schemas
- You want to mix auto-generated and custom fields explicitly
- You're building complex object structures
## Mix Tasks
Generate GraphQL schemas, types, and resolvers from Ecto schemas using Mix tasks:
### Generate from Ecto Schema
```bash
mix gql.gen Accounts lib/my_app/accounts/user.ex
```
This generates:
- `lib/my_app_web/graphql/accounts/types.ex` - Object and input_object types
- `lib/my_app_web/graphql/accounts/schema.ex` - Query and mutation definitions
- `lib/my_app_web/graphql/accounts/resolvers.ex` - Resolver function stubs
### Initialize
```bash
mix gql.gen.init
```
## Generated GraphQL Types
```elixir
object :user do
field(:id, :id)
field(:name, :string)
field(:email, :string)
field(:inserted_at, :datetime)
field(:updated_at, :datetime)
end
input_object :user_params do
field(:id, :id)
field(:name, :string)
field(:email, :string)
field(:inserted_at, :datetime)
field(:updated_at, :datetime)
end
```
## Generated Resolvers
Resolver stubs are created for you to implement your business logic:
```elixir
def list_users(_parent, _args, _resolution) do
{:ok, Accounts.list_users()}
end
def get_user(_parent, %{id: id}, _resolution) do
Accounts.get_user!(id)
end
def create_user(_parent, args, _resolution) do
Accounts.create_user(args)
end
def update_user(_parent, %{id: id} = args, _resolution) do
user = Accounts.get_user!(id)
Accounts.update_user(user, args)
end
```
This preserves the separation between your GraphQL layer and business logic.
## Automatic Schema Integration
Generated modules are automatically imported into your root schema:
**lib/example_web/graphql/types.ex**:
```elixir
defmodule ExampleWeb.Graphql.Types do
use Absinthe.Schema.Notation
# Import generated types here
import_types(ExampleWeb.Graphql.Accounts.Schema)
end
```
**lib/example_web/graphql/schema.ex**:
```elixir
defmodule ExampleWeb.Graphql.Schema do
use Absinthe.Schema
import_types(Absinthe.Type.Custom)
import_types(ExampleWeb.Graphql.Types)
query do
import_fields(:user_queries)
end
mutation do
import_fields(:user_mutations)
end
end
```
No manual wiring required. If these files don't exist, they'll be created for you.
## Type Mapping
Ecto types are intelligently mapped to GraphQL types:
| Ecto Type | GraphQL Type |
| --------------- | ------------ |
| `:binary_id` | `:id` |
| `:string` | `:string` |
| `:integer` | `:integer` |
| `:boolean` | `:boolean` |
| `:utc_datetime` | `:datetime` |
| `:map` | `:json` |
See the [full documentation](https://hexdocs.pm/ecto_graphql) for complete type mapping reference.
## Features
- ✅ **Automatic field extraction** from Ecto schemas
- ✅ **Association support** with Dataloader resolution
- ✅ **Smart type mapping** (Ecto → GraphQL)
- ✅ **Table name singularization** (`users` → `user`)
- ✅ **Auto-integration** with existing schemas
- ✅ **Customizable EEx templates** in `priv/templates`
- ✅ **Incremental updates** — doesn't overwrite existing files
- ✅ **Phoenix-friendly** structure and conventions
## Philosophy
EctoGraphql follows these principles:
- **Generated code is yours** — modify, extend, or refactor as needed
- **No runtime magic** — plain Absinthe code you can read and understand
- **Explicit over clever** — predictable generation, no surprises
- **Single source of truth** — Ecto schemas drive your GraphQL API
If the generated code is hard to read or modify, it doesn't belong here.
## Documentation
Full documentation is available on HexDocs:
[https://hexdocs.pm/ecto_graphql](https://hexdocs.pm/ecto_graphql)
## License
MIT