layout |
---|
page |
- 1 Overview
- 2 Attributes
- 3 Querying
- 4 Configuration
- 5 Relationships
- 6 Generators
- 7 Persisting
- 8 Context
- 9 Concurrency
- 10 Adapters
{% include h.html tag="h2" text="1 Overview" a="overview" %}
The same way a Model
is an abstraction around a database table, a
Resource
is an abstraction around an API endpoint. It holds logic for
querying, persisting, and serializing data.
For a condensed view of the Resource interface, see the cheatsheet.
{% include h.html tag="h2" text="2 Attributes" a="attributes" %}
A Resource is composed of Attributes. Each Attribute has a
name (e.g. first_name
) that corresponds to a JSON key, and a
Type (e.g. string
) that corresponds to a JSON value.
To define an attribute:
{% highlight ruby %} attribute :first_name, :string {% endhighlight %}
{% include h.html tag="h4" text="2.1 Limiting Behavior" a="limiting-behavior" %}
Each attribute consists of five flags: readable
, writable
,
sortable
, filterable
, and schema
. Any of these flags can be turned off:
{% highlight ruby %} attribute :name, :string, sortable: false {% endhighlight %}
Or use only/except
shorthand:
{% highlight ruby %} attribute :name, :string, only: [:sortable] attribute :name, :string, except: [:writable] {% endhighlight %}
The schema
flag is not affected by only/except
options.
This option determines if the attribute is exported to the schema.json.
You might want to allow behavior only if a certain condition is met.
Pass a symbol to guard this behavior via corresponding method, only allowing the
behavior if the method returns true
:
{% highlight ruby %} attribute :name, :string, writable: :admin?
def admin?
end {% endhighlight %}
When guarding the :readable
flag, the method can optionally accept the
model instance being serialized as an argument:
{% highlight ruby %} attribute :name, :string, readable: :allowed?
def allowed?(model_instance) model_instance.internal == false end {% endhighlight %}
{% include h.html tag="h4" text="2.2 Default Behavior" a="default-behavior" %}
By default, attributes are enabled for all behavior. You may want to disable certain behavior globally, for example a read-only API. Use these properties to affect all subclasses:
{% highlight ruby %} self.attributes_readable_by_default = false # default true self.attributes_writable_by_default = false # default true self.attributes_filterable_by_default = false # default true self.attributes_sortable_by_default = false # default true self.attributes_schema_by_default = false # default true {% endhighlight %}
{% include h.html tag="h4" text="2.3 Customizing Display" a="customizing-display" %}
Pass a block to attribute
to customize display:
{% highlight ruby %} attribute :name, :string do @object.name.upcase end {% endhighlight %}
@object
will be an instance of your model.
{% include h.html tag="h4" text="2.4 Types" a="types" %}
Each Attribute has a Type. Each Type defines behavior for
- Reading
- Writing
- Filtering
For each of these, we'll first attempt to coerce the given value to the correct type. If that fails, we will raise an error.
The implementation for each of these actions lives in a Dry Type. Take the :integer_id
type: here we want to render a string, but query with an integer (this is the default for all Resource id
attributes):
{% highlight ruby %} Graphiti::Types[:integer_id]
{% endhighlight %}
You can edit these implementations as you wish. Let's make the :string
type
render an integer:
{% highlight ruby %} Graphiti::Types[:string][:read] = Dry::Types['coercible.integer'] {% endhighlight %}
The built-in Types are:
integer_id
string
integer
big_decimal
float
date
datetime
uuid
string_enum
integer_enum
boolean
hash
array
All but the last 3 have Array doppelgängers: array_of_integers
,
array_of_dates
, etc.
The integer_id
type says, "render as a string, but query as an
integer" and is the default for the id
attribute. The uuid
type says
"this is a string, but query me case-sensitive by default".
{% include h.html tag="h5" text="2.5 Enum Types" a="enum-types" %}
Graphiti provides two built enum types, string_enum
and integer_enum
. These behave
in exactly the same way as the string
and integer
types, respectively, except that
when declaring them as either an attribute or a filter you are required to
pass the allow
option, which is the list of acceptable values for the field:
{% highlight ruby %} attribute :status, :string_enum, allow: ['draft', 'published'] {% endhighlight %}
Or if your attribute is backed by an ActiveRecord, you could reference the values directly
{% highlight ruby %}
class Post < ApplicationRecord enum status: { draft: 0, published: 1 } end
class PostResource < ApplicationResource attribute :status, :string_enum, allow: Post.statuses.keys end {% endhighlight %}
See the section on filter options for more details on allow
behavior
Note: Graphiti does not currently do any value checking on enum fields when writing an attribute, and it still expects that your model layer will validate any data coming in.
{% include h.html tag="h5" text="2.6 Custom Types" a="custom-types" %}
Dry Types supports custom types. Let's register a "capital letters" type:
{% highlight ruby %}
definition = Dry::Types::Nominal.new(String) type = definition.constructor do |input| input.upcase end
Graphiti::Types[:caps_lock] = { params: type, read: type, write: type, kind: 'scalar', canonical_name: :caps_lock, description: 'All capital letters' }
attribute :name, :caps_lock {% endhighlight %}
{% include h.html tag="h2" text="3 Querying" a="querying" %}
Resources must be able to dynamically compose a query that can be run against an arbitrary backend (SQL, NoSQL, service calls, etc). They do this through the concept of scoping.
The best way to understand scoping is to take a look at what happens "under the hood". Here's the simple Resource, where most of the logic is hiding in the Adapter:
{% highlight ruby %} class PostResource < ApplicationResource attribute :title, :string end {% endhighlight %}
Now let's show the long-hand version. This is completely runnable code (we're just overriding the default behavior with an explicit version of the same):
{% highlight ruby %} class PostResource < ApplicationResource filter :title do |scope, value| eq do |scope, value| scope.where(title: value) end end
sort :title do |scope, dir| scope.order(title: dir) end
paginate do |scope, current_page, per_page| scope.page(current_page).per(per_page) end
def base_scope Post.all end
def resolve(scope) scope.to_a end end {% endhighlight %}
Let's break this down the key elements:
{% highlight ruby %} def base_scope Post.all end {% endhighlight %}
Graphiti builds queries just like ActiveRecord: start with a base scope (Post.all
), and alter that scope based on the incoming request. #base_scope
defines our starting point.
{% highlight ruby %} filter :title do |scope, value| eq do |scope, value| scope.where(title: value) end end {% endhighlight %}
When the title
query parameter is present, we alter the scope.
{% highlight ruby %} def resolve(scope) scope.to_a end {% endhighlight %}
The #resolve
method is in charge of actually executing the query
and returning model instances.
In other words, this code is roughly equivalent to:
{% highlight ruby %} scope = Post.all # #base_scope if value = params[:filter].try(:[], :title) scope = scope.where(title: value) # .filter end scope.to_a # #resolve {% endhighlight %}
{% include h.html tag="h3" text="3.1 Query Interface" a="query-interface" %}
Resources can query and persist data without an API request or response. To query, pass a JSONAPI-compliant query hash:
{% highlight ruby %} EmployeeResource.all({ filter: { first_name: 'Jane' }, sort: '-created_at', page: { size: 10, number: 2 } }) {% endhighlight %}
The return value from .all
is a proxy object, similar to
ActiveRecord::Relation
:
{% highlight ruby %}
employees = Employee.all employees.class # ActiveRecord::Relation
employees.map(&:first_name) # => ["Jane", "Joe", ...]
employees = EmployeeResource.all employees.class # Graphiti::ResourceProxy
employees.map(&:first_name) # => ["Jane", "Joe", ...]
employees.data # => [#, #, ...] {% endhighlight %}
This proxy object can render JSONAPI, simple JSON, or XML:
{% highlight ruby %} employees = EmployeeResource.all employees.to_jsonapi employees.to_json employees.to_xml {% endhighlight %}
Use .find
to find a single record by id, raising
Graphiti::Errors::RecordNotFound
if no records are returned:
{% highlight ruby %} employee = EmployeeResource.find(id: 123) employee.data.first_name # => "Jane" {% endhighlight %}
Note: SomeResource.find
returns a ResourceProxy
. To access the model/record proper you will want to make sure you call .data
on the result of find as shown above.
{% include h.html tag="h3" text="3.2 Composing with Scopes" a="composing-with-scopes" %}
{% include h.html tag="h4" text="3.2.1 #base_scope" a="base-scope" %}
Override the #base_scope
method whenever you have logic that should
apply to every query. For example, if we only ever wanted to return
active
Positions:
{% highlight ruby %} def base_scope Position.where(active: true) end {% endhighlight %}
This can be overridden by passing a second argument to Resource.all
:
{% highlight ruby %} class InactivePostsController < PostsController def index posts = PostResource.all(params, Post.where(active: false)) respond_with(posts) end end {% endhighlight %}
{% include h.html tag="h4" text="3.4 Sort" a="sort" %}
Use the sort
DSL to customize sorting behavior.
{% highlight ruby %} sort :name, :string do |scope, direction| scope.order(first_name: direction, last_name: direction) end {% endhighlight %}
If you've already defined a corresponding attribute, you'll be overriding that default behavior (and there is no need to pass a type as the second argument):
{% highlight ruby %} attribute :name, :string
sort :name do |scope, direction|
end {% endhighlight %}
Note:
sort
defines a sort-only attribute. If you want other behavior, like filtering, it's best to define the attribute first.
{% include h.html tag="h5" text="3.4.1 Sort Options" a="sort-options" %}
Pass :only
if you support just a single direction:
{% highlight ruby %} sort :name, only: [:desc] {% endhighlight %}
{% include h.html tag="h4" text="3.5 Filter" a="filter" %}
Use the filter
DSL to customize each operator:
{% highlight ruby %} filter :name, :string do eq do |scope, value| scope.where(first_name: value) end
end {% endhighlight %}
The built-in operators for ActiveRecord are:
- eq (case-insensitive)
- eql (case-sensitive)
- prefix
- suffix
- match
- gt (greater-than)
- gte (greater-than-or-equal-to)
- lt (less-than)
- lte (less-than-or-equal-to)
Note that Graphiti expects filters to support multiple values by default, so
value
will be an array. Passsingle: true
if you do not support multiple values.
To pass multiple values in a query string, comma-delimit:
/employees?filter[name]=Jane,John
If you've already defined a corresponding attribute, you'll be overriding that default behavior (and there is no need to pass a type as the second argument):
{% highlight ruby %} attribute :name, :string
filter :name do eq do |scope, value| # ... code ... end end {% endhighlight %}
You can define custom operators on-the-fly:
{% highlight ruby %} filter :name do fuzzy_match do |scope, value| # ... code ... end end {% endhighlight %}
Will now support filter[name][fuzzy_match]=foo
Note:
filter
defines a filter-only attribute. If you want other behavior, like sorting, it's best to define the attribute first.
{% include h.html tag="h5" text="3.5.1 Filter Options" a="filter-options" %}
Pass :only
or :except
to limit possible operators:
{% highlight ruby %} filter :name, :string, only: [:eq, :suffix] {% endhighlight %}
Pass :allow
or :reject
to only allow filtering on certain values, or
reject bad values:
{% highlight ruby %} filter :size, :string, allow: ['Big', 'Medium', 'Small']
filter :size, :string, reject: ['X-Large'] {% endhighlight %}
By default, all filters accept multiple values, causing the yielded
value
to always be an array. Pass single: true
to only allow a
single value:
{% highlight ruby %}
filter :name, :string do eq do |scope, value| value # => ["Jane"] end end
filter :name, :string, single: true do eq do |scope, value| value # => "Jane" end end {% endhighlight %}
Filters can be required:
{% highlight ruby %}
attribute :customer_id, :integer, filterable: :required
filter :customer_id, :string, required: true {% endhighlight %}
Filters can also depend on other filters, requiring all criteria to be present:
{% highlight ruby %}
filter :customer_id, :integer, dependent: [:customer_type] filter :customer_type, :string, dependent: [:customer_id] {% endhighlight %}
{% include h.html tag="h5" text="3.5.2 Boolean Filter" a="boolean-filter" %}
It doesn't make sense for a filter with type boolean
to accept
multiple values. These filters will be single: true
by default.
{% include h.html tag="h5" text="3.5.3 Hash Filter" a="hash-filter" %}
Filters with type hash
will automatically parse JSON when passed in a
URL query string:
{% highlight ruby %}
filter :metadata, :hash do eq do |scope, value| value # => [{ "foo" => 100 }] end end {% endhighlight %}
{% include h.html tag="h5" text="3.5.4 Escaping Values" a="escaping-values" %}
By default, Graphiti parses a comma-delimited string as an array. There are times you may not want this - for instance a "keyword search" field that could contain a comma.
Wrap values in {% raw %}{{curlies}}
{% endraw %} to avoid parsing:
{% highlight ruby %} {% raw %}
filter :keywords, :string do eq do |scope, value| value # => "some,value" end end {% endraw %} {% endhighlight %}
You can also define arrays explicitly instead of delimiting on comma:
{% highlight ruby %}
filter :keywords, :string do eq do |scope, value| value # => ["some", "value"] end end {% endhighlight %}
If a filter is marked single: true
, we'll avoid any array parsing and
escape the value for you, filtering on the string as given.
By default a value that comes in as null
is treated as a string "null"
.
To coerce null
to a Ruby nil
mark the filter with allow_nil: true
.
This can be changed for all attributes by setting filters_accept_nil_by_default
{% highlight ruby %} class PostResource < ApplicationResource self.filters_accept_nil_by_default = true end {% endhighlight %}
{% include h.html tag="h4" text="3.6 Statistics" a="statistics" %}
Statistics are useful and common. Consider a datagrid listing posts - we might want a "Total Posts" count displayed above the grid without firing an additional request. Notably, that statistic should take into account filtering, but should not take into account pagination.
All resources have a total count statistic by default:
{% highlight ruby %} PostResource.all({ stats: { total: 'count' } }) {% endhighlight %}
/posts?stats[total]=count
Would cause the meta
section of the response to be:
{% highlight ruby %} { meta: { stats: { total: { count: 100 } } } } {% endhighlight %}
Allow a given statistic to be requested using .stat
:
{% highlight ruby %} stat total: [:count] stat rating: [:average] stat likes: [:sum] stat score: [:maximum] stat score: [:maximum]
{% endhighlight %}
You can also define custom statistics:
{% highlight ruby %} stat rating: [:average] do standard_deviation do |scope, attr| # your standard deviation code here end end {% endhighlight %}
{% include h.html tag="h4" text="3.7 Extra Fields" a="extra-fields" %}
Sometimes you have a field that is not always needed, and perhaps computationally expensive. In this case, you only want the field returned when explicitly requested by the client. To do this:
{% highlight ruby %} extra_attribute :net_worth {% endhighlight %}
This works just like attribute
, except the field is read-only and will
only be returned when requested. The query parameter signature matches
fields
: ?extra_fields[employees]=net_worth
.
You may want to adjust your scope to eager load data when a given extra field is requested. To do this:
{% highlight ruby %} resource.on_extra_attribute :net_worth do |scope| scope.includes(:assets) end {% endhighlight %}
{% include h.html tag="h4" text="3.8 #resolve" a="resolve" %}
After we build up a query, we pass it to #resolve
. Resolve must do
two things:
- Execute the query
- Return an array of
Model
instances
Override #resolve
if you need more than the default behavior:
{% highlight ruby %} def resolve(scope) Rails.logger.info "begin resolving scope..." result = super Rails.logger.info "resolved!" result end {% endhighlight %}
{% include h.html tag="h2" text="4 Configuration" a="configuration" %}
Here's a Resource with explicit defaults:
{% highlight ruby %} class PostResource < ApplicationResource self.model = Post self.type = 'posts'
primary_endpoint '/posts', [:index, :show, :create, :update, :destroy]
self.default_sort = [{ title: :asc }]
self.default_page_size = 10 end {% endhighlight %}
Typically you'd inherit from ApplicationResource
. Here are some common higher-level customization options that will affect subclasses:
{% highlight ruby %} class ApplicationResource < Graphiti::Resource
self.abstract_class = true
self.adapter = Graphiti::Adapters::ActiveRecord
self.attributes_readable_by_default = true self.attributes_writable_by_default = true self.attributes_sortable_by_default = true self.attributes_filterable_by_default = true
self.base_url = Rails.application.routes.default_url_options[:host]
self.endpoint_namespace = '/api/v1'
self.validate_endpoints = false
self.autolink = true end {% endhighlight %}
{% include h.html tag="h3" text="4.1 Polymorphic Resources" a="polymorphic-resources" %}
Polymorphic Resources are similar to ActiveRecord STI: when a single query can return multiple Resource instances. We may query /tasks
, but return bugs
, features
, epics
, etc.
For example, given the ActiveRecord
models:
{% highlight ruby %} class Employee < ApplicationRecord has_many :tasks end
class Task < ApplicationRecord belongs_to :employee end
class Bug < Task end
class Feature < Task def points 5 end end
class Epic < Task has_many :milestones end
class Milestone < ApplicationRecord belongs_to :epic end {% endhighlight %}
We could define the following Polymorphic Resources:
{% highlight ruby %} class TaskResource < ApplicationResource
self.polymorphic = [ 'BugResource', 'FeatureResource', 'EpicResource' ]
attribute :title, :string end
class BugResource < TaskResource end
class FeatureResource < TaskResource attribute :points, :integer end
class EpicResource < TaskResource has_many :milestones end
class MilestoneResource < TaskResource belongs_to :epic end {% endhighlight %}
If we hit a /tasks
endpoint, we'd get back JSONAPI types of bugs
, features
and epics
. Only features
would render the points
attribute, and only epics
would render the milestones relationship
.
A query to /tasks?include=milestones
would correctly only query
and render Milestones for Epics.
{% include h.html tag="h2" text="5 Relationships" a="relationships" %}
Resources can connect to other Resources via relationships. Each relationship determines behavior for:
- Sideloading (load both Resources in a single request)
- Links (URL to lazy-load in separate request)
- Sideposting (save both in single request)
When connecting resources, you can imagine the logic similar to
ActiveRecord
's .includes
:
{% highlight ruby %} class PostResource < ApplicationResource has_many :comments end
class CommentResource < ApplicationResource attribute :post_id, :integer, only: [:filterable] belongs_to :post end
PostResource.all(include: 'comments')
CommentResource.all(include: 'post')
{% endhighlight %}
Note the explicit
post_id
filter onCommentResource
{% include h.html tag="h3" text="5.1 Deep Queries" a="deep-queries" %}
A query that applies to a relationship is referred to as a deep query. Use the dot-syntax to deep query:
/employees?include=positions&filter[positions.title]=Manager
/employees?include=positions.department&filter[positions.department.name]=Engineering
The above references the relationship name. For simplicity, you can also pass the JSONAPI type in brackets:
/employees?include=positions.department&filter[departments][name]=Engineering
Sorting and pagination currently only support the JSONAPI type:
/employees?include=positions.department&sort=departments.name
/employees?include=positions.department&page[departments][size]=10
{% include h.html tag="h4" text="5.2 Customizing Relationships" a="customizing-relationships" %}
The default options you can override are:
{% highlight ruby %} has_many :positions, foreign_key: :employee_id, primary_key: :id, resource: EmployeeResource, readable: true, writable: true, link: self.autolink, # default true single: false, # only allow this sideload when one employee always_include_resource_ids: false {% endhighlight %}
note: Setting always_include_resource_ids: true
could result in 1+N queries (see #167)
{% include h.html tag="h5" text="5.2.1 Customizing Scope" a="customizing-scope" %}
Use params
to change the query parameters that will be passed to the
associated Resource:
{% highlight ruby %} has_many :active_positions, resource: PositionResource do params do |hash, employees| hash[:filter][:active] = true end end
{% endhighlight %}
If there is no existing AR association for this we would also need to make it a getter/setter on the model.
{% highlight ruby %}
attr_accessor :active_positions {% endhighlight %}
{% include h.html tag="h5" text="5.2.1 Customizing Assignment" a="customizing-assignment" %}
Once we've fetched primary data and its relationship (e.g. we have an
employees
array and positions
array), we need to associate these
objects:
{% highlight ruby %} employees.each do |e| e.positions = positions.select { |p| p.employee_id == e.id } end {% endhighlight %}
Occasionally this logic will be non-standard or more complex. Use
assign_each
to customize, returning all relevant children for the
given parent:
{% highlight ruby %} has_many :positions do assign_each do |employee, positions| positions.select { |p| p.belongs_to?(employee) } end end {% endhighlight %}
Or if all else fails, use #assign
to control all the logic:
{% highlight ruby %} has_many :positions do assign do |employees, positions| employees.each do |employee| positions.select { |p| p.belongs_to?(employee) } end end end {% endhighlight %}
Note: ActiveRecord will sometimes cause unexpected queries when
assigning. If you're overriding #assign
, make sure to keep an eye on
this. If using #assign_each
, you're fine because the adapter will take
care of this for you.
{% include h.html tag="h4" text="5.3 has_many" a="has-many" %}
{% highlight ruby %} has_many :positions {% endhighlight %}
Defaults to these common options:
{% highlight ruby %} has_many :positions, foreign_key: :employee_id, primary_key: :id, always_include_resource_ids: false, resource: PositionResource {% endhighlight %}
Which would cause the following query when sideloading:
{% highlight ruby %} PositionResource.all({ filter: { employee_id => employee_ids } }) {% endhighlight ruby %}
This means we need to make sure that filter is supported:
{% highlight ruby %} class PositionResource < ApplicationResource attribute :employee_id, :integer, only: [:filterable]
end {% endhighlight ruby %}
Once we've resolved employees
and positions
the resulting objects
would be associated with logic similar to:
{% highlight ruby %} employees.each do |e| e.positions = positions.select { |p| p.employee_id == e.id } end {% endhighlight %}
And generate a Link:
/positions?filter[employee_id]=1,2,3
{% include h.html tag="h4" text="5.4 belongs_to" a="belongs-to" %}
{% highlight ruby %} belongs_to :employee {% endhighlight %}
Defaults to these common options:
{% highlight ruby %} belongs_to :employee, foreign_key: :employee_id, primary_key: :id, always_include_resource_ids: false, resource: EmployeeResource {% endhighlight %}
Which would cause the following query when sideloading:
{% highlight ruby %} EmployeeResource.all({ filter: { id => position_ids } }) {% endhighlight ruby %}
And assign the resulting objects with logic similar to:
{% highlight ruby %} positions.each do |p| p.employee = employees.find { |e| p.employee_id == e.id } end {% endhighlight %}
And generate a Link:
/employees?filter[id]=1,2,3
{% include h.html tag="h4" text="5.5 has_one" a="has-one" %}
has_one
works exactly like has_many
, but only one record will be
returned. When sideloading this will be a single element, much like
belongs_to
.
There is one small caveat: Links always point to an index
action, so
we can apply filters. That means following has_one
Link will lead to
an array, and you should select the first record.
{% include h.html tag="h5" text="5.5.1 Faux has_one" a="faux-has-one" %}
A "Faux Has One" occurs when there is more than one record of
associated data, but we only want to return the first record in that
array. Consider this ActiveRecord
relationship:
{% highlight ruby %}
has_many :positions has_one :current_position, -> { where(created_at: :desc) }, class_name: 'Position'
Employee.includes('current_position').to_a
{% endhighlight %}
When we eager load, more than one Position is returned from the database query. Assigning only the first record and dropping the rest occurs in ruby, not the database query.
The same thing happens in Graphiti:
{% highlight ruby %}
has_many :positions has_one :current_position, resource: PositionResource do params do |hash| hash[:sort] = '-created_at' end end
EmployeeResource.all(include: 'current_position')
})
{% endhighlight %}
Though everything works as expected, a large number of Position records can incur a performance penalty (as we'd be instantiating a large number of ActiveRecord objects).
For this reason, you are encouraged to model Faux Has One's in such a
way that the underlying database query only returns the relevant single
record. Imagine if we had a historical_index
column on positions
,
where a value of 1
meant "most recent":
{% highlight ruby %}
has_many :positions has_one :current_position, -> { where(historical_index: 1) }, class_name: 'Position'
Employee.includes('current_position').to_a
{% endhighlight %}
We've ensured the query itself only returns a single record. Optimizing a Graphiti API is the same as optimizing queries.
{% include h.html tag="h4" text="5.6 many_to_many" a="many-to-many" %}
This relationship is specific to relational databases that use a "join table" between two tables.
Though you can make this work for other ORMs/clients, it's easiest to
explain by focusing on ActiveRecord
.
First, you must use has_many :through and not has_and_belongs_to_many:
{% highlight ruby %} class Employee < ApplicationRecord has_many :team_memberships has_many :teams, through: :team_memberships end
class TeamMembership < ApplicationRecord belongs_to :employee belongs_to :team end
class Team < ApplicationRecord has_many :team_memberships has_many :employees, through: :team_memberships end {% endhighlight %}
You can always expose team_memberships
to your API - particularly
useful if that table holds metadata about the relationship.
Other times, however, clients of the API should not have knowledge of
this implementation detail. In these cases, use many_to_many
:
{% highlight ruby %} class EmployeeResource < ApplicationResource many_to_many :teams end
class TeamResource < ApplicationResource many_to_many :employees end
{% endhighlight %}
The many_to_many
call will automatically add a Filter to the
associated resource. The logic for that filter, in the case of ActiveRecord
:
{% highlight ruby %}
filter :team_id, :integer do eq do |scope, value| scope .includes(:team_memberships) .where(team_memberships: { team_id: value } end end {% endhighlight %}
To customize the foreign key, you will need to specify a hash rather than a symbol. The hash key is the relationship name, so the above is equivalent to
{% highlight ruby %}
many_to_many :teams, foreign_key: { team_memberships: :team_id } {% endhighlight %}
If using ActiveRecord, and the API relationship name does not match your
Model relationship name, use :as
to specify the model relationship
that should be used to derive the query:
{% highlight ruby %}
many_to_many :teams, as: :groups {% endhighlight %}
{% include h.html tag="h4" text="5.7 polymorphic_belongs_to" a="polymorphic-belongs-to" %}
With polymorphic associations, a Resource can belong to more than one other Resource, on a single association. Though these relationships are not specific to ActiveRecord
, we'll use ActiveRecord
conventions to describe the use case.
Given the following polymorphic ActiveRecords:
{% highlight ruby %} class Note < ApplicationRecord belongs_to :notable, polymorphic: true end
class Employee < ApplicationRecord has_many :notes, as: :notable end
class Department < ApplicationRecord has_many :notes, as: :notable end
class Team < ApplicationRecord has_many :notes, as: :notable end {% endhighlight %}
By ActiveRecord
convention, the notes
table would have columns
notable_id
and notable_type
.
Graphiti has the same concept. In this case we would group all the notes
by a given notable_type
, and follow a different belongs_to
association for each group:
{% highlight ruby %}
polymorphic_belongs_to :notable do group_by(:notable_type) do on(:Employee) on(:Department) on(:Team) end end {% endhighlight %}
The on
DSL is shorthand for a belongs_to
relationship that accepts
all the usual options and customizations:
{% highlight ruby %} on(:Employee).belongs_to :employee, resource: EmployeeResource
{% endhighlight %}
In other words: group all Notes by notable_type
, and for all that have
the value of "Employee"
use the belongs_to :employee
relationship
for further querying.
{% include h.html tag="h4" text="5.8 polymorphic_has_many" a="polymorphic-has-many" %}
Continuing from the prior section, the corresponding association of a
polymorphic_belongs_to
is a polymorphic_has_many
:
{% highlight ruby %} class EmployeeResource < ApplicationResource polymorphic_has_many :notes, as: :notable end {% endhighlight %}
Predictably, this causes the query:
{% highlight ruby %} NoteResource.all({ filter: { notable_type: 'Employee', notable_id: employee_ids } }) {% endhighlight %}
And the Link
/notes?filter[notable_id]=1,2,3&filter[notable_type]=Employee
Which means the following filters are required:
{% highlight ruby %} class NoteResource < ApplicationResource attribute :notable_id, :integer, only: [:filterable] attribute :notable_type, :string, only: [:filterable]
end {% endhighlight %}
{% include h.html tag="h2" text="6 Generators" a="generators" %}
To generate a Resource:
{% highlight bash %} $ rails generate graphiti:resource NAME [attribute:type] [options] {% endhighlight %}
For example:
{% highlight bash %} $ rails generate graphiti:resource Employee first_name:string age:integer {% endhighlight %}
Will add a route, controller, resource, and tests.
Limit the actions this resource supports with -a
:
{% highlight bash %} $ rails generate graphiti:resource Employee -a index show {% endhighlight %}
{% include h.html tag="h2" text="7 Persisting" a="persisting" %}
Graphiti allows writing a graph of data in a single request. We'll do the work of parsing the graph and ordering operations, so you can focus on the part you care about: the logic for actually persisting an object.
By default, persistence operations are handled by your adapter. The "expanded" view of the ActiveRecord implementation is below:
{% highlight ruby %}
def create(attributes) employee = Employee.new attributes.each_pair do |key, value| employee.send(:"#{key}=", value) end employee.save employee end
def update(attributes) employee = EmployeeResource.find(attributes.delete(:id)).data attributes.each_pair do |key, value| employee.send(:"#{key}=", value) end employee.save employee end
def destroy(attributes) employee = EmployeeResource.find(attributes.delete(:id)).data employee.destroy employee end {% endhighlight %}
- You are encouraged not to override these directly. Instead, use hooks (see next section).
- We'll process any
writable: false
or guarded attributes prior to these methods. - After these methods, we'll check the Model instance for validation errors, rolling back the transaction if any Model in the graph is invalid.
- These methods must return the Model instance.
{% include h.html tag="h3" text="7.1 Persistence Lifecycle Hooks" a="persistence-lifecycle-hooks" %}
Let's dive into a persistence request. If you look at the code snippets in the prior section, the flow breaks down into 3 steps:
- Build or find the model
- Assign attributes to the model
- Save
You can hook into each step:
{% highlight ruby %} class PostResource < ApplicationResource before_attributes do |attributes| # Before attributes have been assigned to the model end
after_attributes do |model| # After attributes have been assigned to the model end
around_attributes :do_around_attributes
def do_around_attributes(attributes) # before model_instance = yield attributes # after end
before_save do |model| # After attributes assigned, but before persisting end
after_save do |model| # After model has been saved end
around_save :do_around_save
def do_around_save(model) # before yield model # after end
def build(model_class) model_class.new end
def assign_attributes(model_instance, attributes) attributes.each_pair do |key, value| model_instance.send(:"#{key}=", value) end end
def save(model_instance) model_instance.save model_instance end
def delete(model_instance) model_instance.destroy model_instance end
around_persistence :do_around_persistence
def do_around_persistence(attributes) attributes[:foo] = 'bar' model = yield # build/find, assign attrs, save model.update_counter_cache end end {% endhighlight %}
- All hooks have
only/except
options, e.g.before_attributes only: [:update]
- Most hooks can be called with an in-line block, or by passing a method
name (e.g.
before_attriubtes :do_something
). The exception isaround_*
hooks, which must be called with a method name.
When persisting multiple objects at once, we'll open a database transaction, process each model individually, ensure all models pass validation, then close the transaction. This means that if you raise an error at any point, or any model does not pass validations, the transaction will be rolled back.
You may want to perform an operation after all models have been
processed and validated, but before the transaction is closed. One
example is sending an email - you don't want to send if the models were
invalid, so after_save
wouldn't work. And you still want to do it
within the transaction, so if your email server is down and an error
is raised the transaction gets rolled back.
For this scenario, use before_commit
:
{% highlight ruby %} before_commit do |model| PostMailer.with(post: model).some_email.deliver end {% endhighlight %}
{% include h.html tag="h3" text="7.2 Sideposting" a="sideposting" %}
The act of persisting multiple Resources in a single request is called Sideposting. The payload mirrors the sideloading payload for read operations, with minor additions.
Let's create a Post and associate it to an existing Blog in a single request:
{% highlight ruby %}
{ type: 'posts', attributes: { title: 'My post' }, relationships: { blog: { data: { id: '1', type: 'blogs', method: 'update' } } } } {% endhighlight %}
The critical addition here is the method
key. When we persist RESTful
Resources, we send a corresponding HTTP verb. This follows the same
pattern, adding a verb for each Resource in the graph. method
can be
one of:
create
update
destroy
disassociate
(e.g.null
foreign key)
When we sidepost, all objects will be persisted within the same database transaction, which rolls back if an error is raised or any objects are invalid.
{% include h.html tag="h4" text="7.2.1 Create" a="create" %}
Let's say we want to create a Post and its Blog in a single request.
You'll note that we don't have the id
key to generate a Resource
Identifier (combination of id
and type
that uniquely identifies a Resource).
To accomodate this, send an ephemeral temp-id
(any UUID):
{% highlight ruby %} {
{ type: 'posts', attributes: { title: 'My post' }, relationships: { blog: { data: { :'temp-id' => 'abc123', type: 'blogs', method: 'create' } } }, included: [ { :'temp-id' => 'abc123' type: 'blogs', attributes: { name: 'New Blog' } } ] } } {% endhighlight %}
This random UUID:
- Connects relevant sections of the payload.
- Tells clients how to associate their in-memory objects with the ids returned from the server.
{% include h.html tag="h4" text="7.2.2 Expanded Example" a="expanded-example" %}
Here we're updating a Post, changing the name of its associated Blog, creating a Tag, deleting one Comment, and disassociating (null
foreign key) a different Comment, all in a single request:
{% highlight ruby %} { data: { type: 'posts', id: 123, attributes: { title: 'Updated!' }, relationships: { blog: { data: { type: 'blogs', id: 123, method: 'update' } }, tags: { data: [{ type: 'tags', temp-id: 's0m3uu1d', method: 'create' }] }, comments: { data: [ { type: 'comments', id: '123', method: 'destroy' }, { type: 'comments', id: '456', method: 'disassociate' } ] } } }, included: [ { type: 'tags', :'temp-id' => 's0m3uu1d', attributes: { name: 'Important' } }, { type: 'blogs', id: => '123', attributes: { name: 'Updated!' } } ] } {% endhighlight %}
{% include h.html tag="h3" text="7.3 Validation Errors" a="validation-errors" %}
When a persistence operation is attempted but the corresponding Resource
is invalid, the transaction will be rolled back and an errors payload will be returned
with a 422
response code:
{% highlight ruby %} { errors: [{ code: 'unprocessable_entity', status: '422', title: "Validation Error", detail: "Title can't be blank", source: { pointer: '/data/attributes/title' }, meta: { attribute: :title, message: "can't be blank", code: :blank } }] } {% endhighlight %}
To get this functionality, your Model must adhere to the ActiveModel::Validations API.
You get this for free with ActiveRecord, or it can be mixed in to any PORO:
{% highlight ruby %} class Post include ActiveModel::Validations validates :title, presence: true end {% endhighlight %}
Errors on associations will have a slightly expanded payload:
{% highlight ruby %} { errors: [{ code: 'unprocessable_entity', status: '422', title: 'Validation Error', detail: "Name can't be blank", source: { pointer: '/data/attributes/name' }, meta: { relationship: { attribute: :name, message: "can't be blank", code: :blank, name: :pets, id: '444', type: 'pets' } } }] } {% endhighlight %}
When Sideposting, the errors payload will contain all invalid Resources in the graph.
{% include h.html tag="h2" text="7.4 Read on Write" a="read-on-write" %}
By default, the response of a persistence operation will mirror your request. But sometimes you need control over the response. The most common scenario is sideloading an additional entity - imagine creating an order, and wanting the order's shipping information to come back in the response.
You can do this by POSTing the payload as normal, but adding query parameters to the URL:
{% highlight ruby %}
{ type: 'orders', attributes: { ... } } {% endhighlight %}
This will sideload the shipping information in the response. When using Spraypaint, do this with:
{% highlight typescript %} order.save({ returnScope: Order.includes('shipping_information') }) {% endhighlight %}
{% include h.html tag="h2" text="8 Context" a="context" %}
All resources have access to #context
. If you're using Rails,
context
is the controller instance processing the request.
{% highlight ruby %}
attribute :active, :boolean, writable: :admin?
def admin? context.current_user.admin? end {% endhighlight %}
Because current_user
is so common, we recommend putting this in
ApplicationResource
:
{% highlight ruby %}
class ApplicationResource < Graphiti::Resource
def current_user context.current_user end end
class PostResource < ApplicationResource
def admin? current_user.admin? end end {% endhighlight %}
You can manually set context with with_context
:
{% highlight ruby %} ctx = OpenStruct.new(current_user: User.first) Graphiti.with_context(ctx) do
PostResource.all end {% endhighlight %}
{% include h.html tag="h2" text="9 Concurrency" a="concurrency" %}
By default when using Rails, Graphiti will turn on concurrency when ::Rails.application.config.cache_classes
is true
(the default for staging and production environments). This will cause sibling sideloads to load concurrently. If a Post
is sideloading Comments
and Author
, we'll load both of those at the same time.
You can turn on/off this behavior explicitly:
{% highlight ruby %}
Graphiti.configure do |c| c.concurrency = false end {% endhighlight %}
NOTE: Since this kicks off a new Thread, thread locals will be dropped. So if your code refers to Thread.current[:foo]
you should set and get that on Graphiti.context
:
{% highlight ruby %}
Thread.current[:foo] = "bar" Thread.current[:foo] # => will be nil when sideloading!
Graphiti.context[:foo] = "bar" Graphiti.context[:foo] # => "bar", even when sideloading {% endhighlight %}
{% include h.html tag="h2" text="10 Adapters" a="adapters" %}
Common resource overrides can be packaged into an Adapter for code re-use. The most common example is using a different client/datastore than ActiveRecord/RelationalDB.
Adapters are best explained in our 'Without ActiveRecord' Cookbook.