Skip to main content

Overview

The ActiveRecord source provides seamless integration between ActiveRecord models and GraphQL. It automatically generates objects, inputs, queries, and mutations based on your model definitions, supporting PostgreSQL, MySQL, and SQLite.
ActiveRecord sources handle associations, validations, enums, and STI (Single Table Inheritance) automatically.

Basic Setup

# app/graphql/sources/user_source.rb
module GraphQL
  class UserSource < GraphQL::Source::ActiveRecordSource
    build_all
  end
end
This automatically creates:
  • 1 Object type (or Interface for STI)
  • 1 Input type
  • 2 Query fields (singular and plural)
  • 3 Mutation fields (create, update, delete)

Assignment Validation

The source validates it’s used with ActiveRecord models:
# From lib/rails/graphql/source/active_record_source.rb
validate_assignment('ActiveRecord::Base') do |value|
  +"The \"#{value.name}\" is not a valid Active Record model"
end

Database Adapter Support

Automatic adapter enabling based on your database:
step(:start) { GraphQL.enable_ar_adapter(adapter_name) }
Supported adapters:
  • PostgreSQL - Full support including JSONB, arrays, and custom types
  • MySQL - Complete MySQL/MariaDB support
  • SQLite - SQLite3 support for development and testing

Generated Components

Object Type

Builds fields from columns and associations:
step(:object) do
  build_attribute_fields(self)
  build_reflection_fields(self)
end
Example for User model:
type User {
  id: ID!
  name: String!
  email: String!
  createdAt: String!
  posts: [Post!]!
}

Input Type

Creates input from writable attributes:
step(:input) do
  extra = GraphQL.enumerate(primary_key).entries.product([{ null: true }]).to_h
  build_attribute_fields(self, **extra)
  build_reflection_inputs(self)
  
  safe_field(model.inheritance_column, :string, null: false) if interface?
  safe_field(:_delete, :boolean, default: false)
end
Example:
input UserInput {
  id: ID
  name: String!
  email: String!
  _delete: Boolean
}
The _delete field enables cascading deletes in nested mutations.

Query Fields

step(:query) do
  interface? ? build_interface : build_object
  type = interface? ? interface : object
  
  safe_field(plural, type, full: true) do
    before_resolve(:load_records)
  end
  
  safe_field(singular, type, null: false) do
    build_primary_key_arguments(self)
    before_resolve(:load_record)
  end
end
Generated queries:
type Query {
  users: [User!]!
  user(id: ID!): User!
}

Mutation Fields

step(:mutation) do
  interface? ? build_interface : build_object
  type = interface? ? interface : object
  build_input
  
  safe_field("create_#{singular}", type, null: false) do
    argument(singular, input, null: false)
    perform(:create_record)
  end
  
  safe_field("update_#{singular}", type, null: false) do
    build_primary_key_arguments(self)
    argument(singular, input, null: false)
    before_resolve(:load_record)
    perform(:update_record)
  end
  
  safe_field("delete_#{singular}", :boolean, null: false) do
    build_primary_key_arguments(self)
    before_resolve(:load_record)
    perform(:destroy_record)
  end
end
Generated mutations:
type Mutation {
  createUser(user: UserInput!): User!
  updateUser(id: ID!, user: UserInput!): User!
  deleteUser(id: ID!): Boolean!
}

Associations

Associations are automatically converted to fields:
class User < ApplicationRecord
  has_many :posts
end

# Generates:
# field :posts, [Post!]!

Preloading Associations

# From lib/rails/graphql/source/active_record_source.rb
def preload_association(association, scope = nil)
  event.stop(preload(association, scope || event.last_result), layer: :object)
end

def build_association_scope(association)
  scope = model._reflect_on_association(association).klass.default_scoped
  inject_scopes(scope, :relation)
end
Associations use ActiveRecord’s preloader to avoid N+1 queries automatically.

Enums

ActiveRecord enums are converted to GraphQL enums:
class User < ApplicationRecord
  enum role: { user: 0, admin: 1, moderator: 2 }
end
step(:enums) { build_enum_types }

def enums
  @enums ||= model.defined_enums.dup
end
Generated enum:
enum UserRole {
  USER
  ADMIN
  MODERATOR
}

Validations

Presence validations automatically set null: false:
# From lib/rails/graphql/source/active_record_source.rb
def attr_required?(attr_name)
  return true if attr_name.eql?(primary_key)
  return false if model.columns_hash[attr_name]&.default.present?
  return false unless model._validators.key?(attr_name.to_sym)
  
  model._validators[attr_name.to_sym].any? do |validator|
    validator.is_a?(presence_validator) &&
      !(validator.options[:if] || validator.options[:unless])
  end
end

STI (Single Table Inheritance)

STI models generate interfaces instead of objects:
def interface?
  defined?(@interface) || act_as_interface == true ||
    (act_as_interface != false && sti_interface?)
end
class Animal < ApplicationRecord
  # STI base
end

# Generates Interface

Error Handling

Validation errors can be exposed in extensions:
class UserSource < GraphQL::Source::ActiveRecordSource
  self.errors_to_extensions = :messages  # or :details
end
# From lib/rails/graphql/source/active_record_source.rb
def create_record
  input_argument.resource.tap(&:save!)
rescue ::ActiveRecord::RecordInvalid => error
  errors_to_extensions(error.record.errors)
  raise
end
Response with errors:
{
  "data": { "createUser": null },
  "errors": [{
    "message": "Validation failed: Email can't be blank"
  }],
  "extensions": {
    "createUser": {
      "email": ["can't be blank"]
    }
  }
}

Customization

class UserSource < GraphQL::Source::ActiveRecordSource
  skip_fields! :password_digest, :reset_token
  skip_from :input, :created_at, :updated_at
  
  build_all
end

Configuration Options

with_associations
boolean
default:"true"
Build fields for associations
errors_to_extensions
false | :messages | :details
default:"false"
Export validation errors to response extensions
act_as_interface
boolean
default:"nil"
Force interface generation instead of object
interface_class
Class
default:"nil"
Custom superclass for generated interface

Performance

ActiveRecord sources include built-in optimizations:

N+1 Prevention

Automatic association preloading using ActiveRecord::Associations::Preloader

Lazy Loading

Tables are only checked when building: build!(*) super if model&.table_exists?

Scoped Queries

Support for default scopes and injected scopes

Sources

Core source concepts and architecture

Type Map

Type resolution and registration

Build docs developers (and LLMs) love