Resources
- 1 Overview
- 2 Attributes
- 3 Querying
- 4 Configuration
- 5 Relationships
- 6 Generators
- 7 Persisting
- 8 Context
- 9 Concurrency
- 10 Adapters
1 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.
2 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:
2.1 Limiting Behavior
Each attribute consists of five flags: readable
, writable
,
sortable
, filterable
, and schema
. Any of these flags can be turned off:
Or use only/except
shorthand:
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
:
When guarding the :readable
flag, the method can optionally accept the
model instance being serialized as an argument:
2.2 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:
2.3 Customizing Display
Pass a block to attribute
to customize display:
@object
will be an instance of your model.
2.4 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):
You can edit these implementations as you wish. Let’s make the :string
type
render an integer:
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”.
2.5 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:
Or if your attribute is backed by an ActiveRecord, you could reference the values directly
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.
2.6 Custom Types
Dry Types supports custom types. Let’s register a “capital letters” type:
3 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:
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):
Let’s break this down the key elements:
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.
When the title
query parameter is present, we alter the scope.
The #resolve
method is in charge of actually executing the query
and returning model instances.
In other words, this code is roughly equivalent to:
3.1 Query Interface
Resources can query and persist data without an API request or response. To query, pass a JSONAPI-compliant query hash:
The return value from .all
is a proxy object, similar to
ActiveRecord::Relation
:
This proxy object can render JSONAPI, simple JSON, or XML:
Use .find
to find a single record by id, raising
Graphiti::Errors::RecordNotFound
if no records are returned:
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.
3.2 Composing with Scopes
3.2.1 #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:
This can be overridden by passing a second argument to Resource.all
:
3.4 Sort
Use the sort
DSL to customize sorting behavior.
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):
Note:
sort
defines a sort-only attribute. If you want other behavior, like filtering, it’s best to define the attribute first.
3.4.1 Sort Options
Pass :only
if you support just a single direction:
3.5 Filter
Use the filter
DSL to customize each operator:
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):
You can define custom operators on-the-fly:
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.
3.5.1 Filter Options
Pass :only
or :except
to limit possible operators:
Pass :allow
or :reject
to only allow filtering on certain values, or
reject bad values:
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:
Filters can be required:
Filters can also depend on other filters, requiring all criteria to be present:
3.5.2 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.
3.5.3 Hash Filter
Filters with type hash
will automatically parse JSON when passed in a
URL query string:
3.5.4 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 {{curlies}}
to avoid parsing:
You can also define arrays explicitly instead of delimiting on comma:
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
3.6 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:
/posts?stats[total]=count
Would cause the meta
section of the response to be:
Allow a given statistic to be requested using .stat
:
You can also define custom statistics:
3.7 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:
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:
3.8 #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:
4 Configuration
Here’s a Resource with explicit defaults:
Typically you’d inherit from ApplicationResource
. Here are some common higher-level customization options that will affect subclasses:
4.1 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:
We could define the following Polymorphic Resources:
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.
5 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
:
Note the explicit
post_id
filter onCommentResource
5.1 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
5.2 Customizing Relationships
The default options you can override are:
note: Setting always_include_resource_ids: true
could result in 1+N queries (see #167)
5.2.1 Customizing Scope
Use params
to change the query parameters that will be passed to the
associated Resource:
If there is no existing AR association for this we would also need to make it a getter/setter on the model.
5.2.1 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:
Occasionally this logic will be non-standard or more complex. Use
assign_each
to customize, returning all relevant children for the
given parent:
Or if all else fails, use #assign
to control all the logic:
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.
5.3 has_many
Defaults to these common options:
Which would cause the following query when sideloading:
This means we need to make sure that filter is supported:
Once we’ve resolved employees
and positions
the resulting objects
would be associated with logic similar to:
And generate a Link:
/positions?filter[employee_id]=1,2,3
5.4 belongs_to
Defaults to these common options:
Which would cause the following query when sideloading:
And assign the resulting objects with logic similar to:
And generate a Link:
/employees?filter[id]=1,2,3
5.5 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.
5.5.1 Faux has_one
A “Faux Has One” occurrs 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:
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:
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”:
We’ve ensured the query itself only returns a single record. Optimizing a Graphiti API is the same as optimizing queries.
5.6 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:
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
:
The many_to_many
call will automatically add a Filter to the
associated resource. The logic for that filter, in the case of ActiveRecord
:
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
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:
5.7 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:
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:
The on
DSL is shorthand for a belongs_to
relationship that accepts
all the usual options and customizations:
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.
5.8 polymorphic_has_many
Continuing from the prior section, the corresponding association of a
polymorphic_belongs_to
is a polymorphic_has_many
:
Predictably, this causes the query:
And the Link
/notes?filter[notable_id]=1,2,3&filter[notable_type]=Employee
Which means the following filters are required:
6 Generators
To generate a Resource:
For example:
Will add a route, controller, resource, and tests.
Limit the actions this resource supports with -a
:
7 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:
- 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.
7.1 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:
- 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
:
7.2 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:
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.
7.2.1 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):
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.
7.2.2 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:
7.3 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:
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:
Errors on associations will have a slightly expanded payload:
When Sideposting, the errors payload will contain all invalid Resources in the graph.
7.4 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:
This will sideload the shipping information in the response. When using Spraypaint, do this with:
8 Context
All resources have access to #context
. If you’re using Rails,
context
is the controller instance processing the request.
Because current_user
is so common, we recommend putting this in
ApplicationResource
:
You can manually set context with with_context
:
9 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:
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
:
10 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.