Backends and Models
A Resource queries a Backend and returns Models from what comes back. Graphiti serializes the Models.
With ActiveRecord those are the same object. Employee is both the thing you query and the thing you render, and you can skip most of this page. It matters when they're separate: a search index, an HTTP service, a document store. Then the Backend is whatever you query, and the Model is whatever you hand back.
Scopes
A scope is whatever your backend needs to run a query. Graphiti doesn't care what it is. For ActiveRecord it's an ActiveRecord::Relation. Here it's a plain hash:
class EmployeeResource < ApplicationResource
self.adapter = Graphiti::Adapters::Null
attribute :name, :string
def base_scope
{ conditions: {}, sort: {} }
end
filter :name do
eq do |scope, value|
scope[:conditions].merge!(value)
scope
end
end
sort :name do |scope, direction|
scope[:sort] = { name: direction }
scope
end
def resolve(scope)
results = Backend.query(scope)
results.map { |result| Employee.new(result) }
end
end
base_scope is the starting point, each filter and sort block mutates it based on request params, and resolve runs the query and returns Models.
Every block must return the scope. Returning the result of merge! or an assignment instead of the scope itself is the most common way to break this.
Writing that per Resource gets old. Once the pattern stabilizes, move it into an Adapter and Resources go back to being declarative:
class EmployeeResource < ApplicationResource
self.adapter = BackendAdapter
attribute :name, :string
end
What a Model has to do
Respond to id, uniquely. Graphiti uses model.id to tell records apart when rendering. Duplicate ids produce wrong output, not an error.
If the underlying record has no id, generate one:
def id
@id ||= SecureRandom.uuid
end
Respond to its readable attributes. attribute :name, :string calls model.name. If your Model doesn't have that method, pass a block instead:
attribute :name, :string do
@object.full_name
end
Include ActiveModel::Validations if you want validation errors. Graphiti checks models on write requests and renders a JSON:API errors payload from model.errors. Without it, an invalid model saves silently:
class Employee
include ActiveModel::Validations
validates :name, presence: true
end
Writing a Model
Graphiti has no opinion here. A plain class works:
class Employee
attr_accessor :id, :first_name, :last_name, :age
def initialize(attrs = {})
attrs.each_pair { |key, value| send(:"#{key}=", value) }
end
end
ActiveModel::Model gives you the constructor and validations for free:
class Employee
include ActiveModel::Model
attr_accessor :id, :first_name, :last_name, :age
end
Dry::Struct adds type enforcement, and dry-types is already a Graphiti dependency:
class Employee < Dry::Struct
attribute :id, Types::Integer
attribute :first_name, Types::String
attribute :last_name, Types::String
attribute :age, Types::Integer
end
OpenStruct also works and is what Graphiti uses internally for remote resources, but it fails quietly in ways the others don't. See OpenStruct Models before reaching for it.