Sources in Rails GraphQL provide a powerful abstraction for bridging data models to your GraphQL schema. They automatically generate objects, inputs, queries, and mutations based on your underlying data structure.
A source is an abstract object that contains fields, objects, and operations which are delivered to your schemas through proxies. Sources maintain ownership of objects while making them available across multiple schemas and namespaces.
module GraphQL class CustomSource < Rails::GraphQL::Source::Base # Assign the source to a model/class assigned_to 'MyDataModel' # Configure namespaces namespace :api # Define hooks step(:object) do # Build object type fields end step(:query) do # Build query fields end endend
class CustomSource < Rails::GraphQL::Source::Base # By class name (string) assigned_to 'MyDataModel' # By constant assigned_to MyDataModel # Auto-inferred from source name # UserSource -> User # BlogPostSource -> BlogPostend
step(:object) do # Add fields to the object type safe_field :name, :string, null: false safe_field :email, :stringendstep(:query) do # Build the object first build_object # Add query fields safe_field(:user, object, null: false) do argument :id, :id, null: false before_resolve(:load_user) endend
module GraphQL class RestApiSource < Rails::GraphQL::Source::Base class_attribute :endpoint, instance_accessor: false class_attribute :model_class, instance_accessor: false # Set the API endpoint def self.api_endpoint(url) self.endpoint = url end # Build object with API fields step(:object) do build_fields_from_schema end # Build query fields step(:query) do build_object safe_field(plural_name, [object]) do before_resolve(:fetch_collection) end safe_field(singular_name, object, null: false) do argument :id, :id, null: false before_resolve(:fetch_single) end end # Build mutation fields step(:mutation) do build_object build_input safe_field("create_#{singular_name}", object, null: false) do argument singular_name, input, null: false perform(:create_via_api) end safe_field("update_#{singular_name}", object, null: false) do argument :id, :id, null: false argument singular_name, input, null: false perform(:update_via_api) end end class << self def singular_name base_name.underscore end def plural_name singular_name.pluralize end private def build_fields_from_schema # Introspect API schema and build fields api_schema.each do |field_name, field_type| safe_field field_name, field_type end end def api_schema # Return field definitions from API HTTParty.get("#{endpoint}/schema").parsed_response end end # Instance methods for resolvers def fetch_collection response = HTTParty.get(self.class.endpoint) response.parsed_response.map { |data| model_class.new(data) } end def fetch_single id = event.argument(:id) response = HTTParty.get("#{self.class.endpoint}/#{id}") model_class.new(response.parsed_response) end def create_via_api data = event.argument(self.class.singular_name) response = HTTParty.post(self.class.endpoint, body: data.to_h) model_class.new(response.parsed_response) end def update_via_api id = event.argument(:id) data = event.argument(self.class.singular_name) response = HTTParty.patch("#{self.class.endpoint}/#{id}", body: data.to_h) model_class.new(response.parsed_response) end endend
module GraphQL class MongoSource < Rails::GraphQL::Source::Base self.abstract = true # Build object fields from Mongoid model step(:object) do model.fields.each do |name, field| next if skip_field?(name, on: :object) type = map_mongo_type(field.type) safe_field name, type, null: !field.required? end end # Build input fields step(:input) do model.fields.each do |name, field| next if skip_field?(name, on: :input) next if name == '_id' type = map_mongo_type(field.type) argument name, type, null: !field.required? end end # Build query fields step(:query) do build_object safe_field(plural, [object]) do argument :limit, :int, default: 100 before_resolve(:load_documents) end safe_field(singular, object, null: false) do argument :id, :id, null: false before_resolve(:load_document) end end # Build mutation fields step(:mutation) do build_object build_input safe_field("create_#{singular}", object, null: false) do argument singular, input, null: false perform(:create_document) end safe_field("update_#{singular}", object, null: false) do argument :id, :id, null: false argument singular, input, null: false perform(:update_document) end safe_field("delete_#{singular}", :boolean, null: false) do argument :id, :id, null: false perform(:delete_document) end end class << self delegate :model_name, to: :model delegate :singular, :plural, to: :model_name alias model assigned_class private def map_mongo_type(mongo_type) case mongo_type.to_s when 'String' then :string when 'Integer' then :int when 'Float' then :float when 'Boolean', 'Mongoid::Boolean' then :boolean when 'Date' then :date when 'DateTime', 'Time' then :date_time when 'Hash' then :json else :string end end end # Resolver methods def load_documents limit = event.argument(:limit) || 100 model.limit(limit).to_a end def load_document id = event.argument(:id) model.find(id) end def create_document data = event.argument(singular).to_h model.create!(data) end def update_document id = event.argument(:id) data = event.argument(singular).to_h document = model.find(id) document.update!(data) document end def delete_document id = event.argument(:id) document = model.find(id) document.destroy! end private def model self.class.model end def singular self.class.singular end endend
Usage:
module GraphQL class ArticleSource < MongoSource assigned_to Article # Mongoid model endend
step(:object) do # self is the source class # Access class methods and attributes assigned_class.columns.each do |column| safe_field column.name, map_type(column.type) endend
RSpec.describe GraphQL::UserSource do describe '.object' do it 'creates object type with expected fields' do expect(described_class.object.field_names).to include(:name, :email) end end describe '.schemas' do it 'belongs to correct namespaces' do expect(described_class.schemas.map(&:namespace)).to include(:api) end end describe '#load_user' do it 'loads user by ID' do user = create(:user) source = described_class.new(event: double(argument: user.id)) expect(source.load_user).to eq(user) end endend