Usage Without ActiveRecord
Graphiti was built to be used with any ORM or datastore, from PostgreSQL
to elasticsearch to Net::HTTP
. In fact, Graphiti itself is tested with
Plain Old Ruby Objects (POROs).
This cookbook will show how to customize a resource around a particular datastore, and how to package those customizations into a reusable adapter. We’ll use an in-memory datastore and Plain Old Ruby Objects (POROs) here, but the lessons apply to any datastore.
For working code, see this branch of the sample application.
We’ll start with this PORO model:
And this in-memory datastore:
Resource Overrides
If it’s your first time with a new ORM or datastore, we recommend putting the logic in the Resource first. Once things are working and there are multiple uses of the same overrides, package them into an Adapter.
Here we’re using the Null
adapter, which acts as a dumb pass-through.
This can be helpful when you just want to get running for a simple use
case and don’t want errors around features you haven’t implemented yet.
But it can also be confusing when you expect certain codepaths to
be hit. Mostly just be aware of Null
’s behavior, or use
Graphiti::Adapters::Abstract
to get helpful errors around what’s not
implemented.
We’re also supplying an explicit base_scope
. This is the beginning
query object we’ll modify as params come in. In the case of
ActiveRecord, we might want an ActiveRecord::Relation
like
Post.all
. For our example, we’ll modify a simple ruby hash (keep in
mind the premise of building a hash of options and passing it off to a
client can apply to any datastore).
Finally, we’re resolving that scope,
returning the full dataset for now. The contract of #resolve
is to
return an array of model instances, hence DATA.map { |d| Post.new(d)
}
.
Sorting
We modified the base scope with a default hash key, :sort
. When the
user requests sorting, we record this by merging into the hash. We can
then reference that information on the scope when resolving.
Note the sort_all
scope block, in fact all scope blocks, must return the scope.
Paginating
Again: merge into the scope, then reference the scope data when resolving.
Filtering
Same as above examples. Again, note that we must return the scope object from the filter function.
Persisting
All at once:
These are the overrides for persistence operations. You are encouraged
not to override create/update/destroy
directly and instead use
Persistence Lifecycle Hooks.
Adapters
OK so we have all our read and write operations working correctly. But if we had multiple Resources all using an in-memory datastore, you’d see this logic repeated all over the place. Let’s create an adapter to DRY up this logic.
There isn’t much more to do than copy/paste what we’ve already done.
Let’s start with our base_scope
, sorting, and pagination:
There’s really nothing here we haven’t seen before. We’re taking the
code we originally wrote, and sticking it into the interface defined by
Graphiti::Adapters::Abstract
.
There’s a little more to do with filtering:
The logic is the same, but we have a separate method for each filter
operator. This allows us to query differently based on the type - for
instance, ActiveRecord will default to case-insensitive for strings, but
straight equality for integers. If you don’t need operator-specific
logic, just alias
as you see here.
You may want to limit the default operators we expect to work with a
given type. Let’s say your backend allows straight equality for strings,
but doesn’t support prefix
, suffix
, etc. You can specify this in
your adapter:
That’s it for reads. For writes, I’ll post the entire adapter code below - again, it’s just copy/pasting what we already wrote into a slightly different format.
That’s really it. See the working code in Employee Directory here.