Sinja::Sequel

Gem Version Dependency Status

Sinja::Sequel configures your Sinja application to work with Sequel out of the box, and provides additional helpers to greatly simplify the process of writing the more complex action helpers (specifically replace, merge, and subtract). An optional extension enhances Sinja's DSL to generate basic action helpers that can be overridden, customized, or removed.

The core configuration and helpers are in pretty good shape (Sinja uses them in its demo app and test suite), but the extension could use some fleshing out. Testers and community contributions welcome!

Installation

Add this line to your application's Gemfile:

gem 'sinja-sequel'

And then execute:

$ bundle

Or install it yourself as:

$ gem install sinja-sequel

Best Practices

Always return Sequel datasets (instead of arrays of objects) from your index (e.g. Foo.dataset) and fetch (e.g. resource.bars_dataset) action helpers. The finalize helper, described below, will ensure they are "rasterized" before being passed to JSONAPI::Serializers.

You'll want to enable Sequel's :tactical_eager_loading plugin for the best performance with JSONAPI::Serializers. I've seen it reduce complex serializations by a factor of 100 (i.e. quite literally one query instead of 100).

If you want to use client-generated IDs, enable the :update_primary_key plugin on the model and call unrestrict_primary_key in the model definition to allow mass assignment (e.g. with Sequel::Model#set_fields).

If your model has foreign keys and you want to enforce a non-nullable constraint at the application level, consider enabling the :validation_helpers plugin on the model and using validates_not_null in conjuction with the validate! helper described below:

class Bar < Sequel::Model
  plugin :validation_helpers

  def validate
    super
    validates_not_null :foo
  end
end

See "Avoiding Null Foreign Keys" in the Sinja documentation for more information.

Finally, enable the :pagination extension on your connection (before prepending Core) to enable pagination!

Usage

Progressively opt-in to Sinja::Sequel's features by enabling (1) Core, (2) Helpers and Core (the most common use-case), or (3) Extension, Helpers, and Core. (Pagination is automatically enabled with Core, but may need to be manually enabled under certain circumstances, detailed below.)

Core

Prepend Sinja::Sequel::Core after registering Sinja:

require 'sinja'
require 'sinja/sequel/core'

class MyApp < Sinatra::Base
  register Sinja

  helpers do
    prepend Sinja::Sequel::Core
  end

  # ..

  freeze_jsonapi
end

Note that you must use prepend (instead of including Sinja::Sequel::Core like a normal module of Sinatra helpers) in order to ensure that the included methods take precedence over Sinja's method stubs (e.g. transaction). This will hopefully be fixed in a future version of Sinatra.

Prepending Core has the following effects on your application:

  • Configures conflict_, not_found_, and validation_exceptions, and validation_formatter.
  • Defines a database helper that delegates to Sequel::Model.db.
  • Defines a transaction helper that delegates to database.transaction.
  • Defines a validate! helper that raises an error if resource is invalid after a create or update action helper invocation.
  • Defines a simple equality-based filter helper that passes the filter params to Sequel::Dataset#where.
  • Defines a sort helper that applies Sequel.asc and Sequel.desc to the sort terms and passes them to Sequel::Dataset#order.
  • Defines a finalize helper that simply calls Sequel::Dataset#all.

If the :pagination Sequel extension is loaded, it also does the following:

  • Configures page_using for page number- and size-based pagination, with an additional record count parameter to avoid repetitive SELECT COUNT queries while paging.
  • Defines a page helper that calls Sequel::Dataset#paginate and computes a hash of page params that Sinja will use to construct the root pagination links and add to the root metadata of the response.

You may override any of the installed helpers by defining your own. Please see the Sinja documentation for more information about Sinja hooks and configurables, and the Sequel documentation for more information about Sequel plugins and features.

Helpers

Include Sinja::Sequel::Helpers after registering Sinja:

require 'sinja'
require 'sinja/sequel/helpers'

class MyApp < Sinatra::Base
  register Sinja

  helpers Sinja::Sequel::Helpers

  # ..

  freeze_jsonapi
end

Note that including Helpers will automatically prepend Core!

next_pk

A convenience method to always return the primary key of the resource and the resource from your create action helpers. Simply use it instead of next!

create do |attr|
  next_pk Foo.create(attr)
end

add_missing

Take the key of a Sequel *_to_many association and an array of resource identifier objects and add the "missing" records to the collection. Makes writing your merge action helpers a breeze!

has_many :bars do
  merge do |rios|
    add_missing(:bars, rios)
  end
end

It will try to cast the ID of each resource identifier object by sending it the :to_i method; pass in a third argument to specify a different method (e.g. if the primary key of the bars table is a varchar, pass in :to_s instead).

This helper also takes an optional block that can be used to filter subresources during processing. Simply return a truthy or falsey value from the block (or raise an error to abort the entire transaction):

has_many :bars do
  merge do |rios|
    add_missing(:bars, rios) do |bar|
      role?(:admin) || bar.owner == resource.owner
    end
  end
end

remove_present

Like add_missing, but removes the "present" records from the collection. Makes writing your subtract action helpers a breeze!

add_remove

Like add_missing and remove_present, but performs an efficient delta operation on the collection. Makes writing your replace action helpers a breeze!

An optional block passed to this method will be used to filter both adds and removes. To use different filters for the two operations, pass a hash of callables (with keys :add and/or :remove).

Extension

Register Sinja::Sequel after registering Sinja:

require 'sinja'
require 'sinja/sequel'

class MyApp < Sinatra::Base
  register Sinja
  register Sinja::Sequel

  # ..

  freeze_jsonapi
end

Note that registering the extension will automatically include Helpers and prepend Core!

After registering the extension, the resource, has_many, and has_one DSL keywords will generate basic action helpers.

  • resource and has_many, and has_one take an optional second argument that specifies the method to use to cast the ID of the resource or resource identifier object(s) (:to_i by default).

  • The generated action helpers will be unrestricted by default.

  • The generated create action helper does not support client-generated IDs.

These action helpers can be subsequently overridden, customized by setting action helper options (i.e. :roles) and/or defining before_<action> hooks, or removed entirely with remove_<action>.

Given a database connection and Foo, Bar, and Qux models and serializers, here's an example "classic"-style application using the extension:

require 'sinatra/jsonapi/sequel'

resource :foos do
  has_many :bars
  has_one :qux
end

resource :bars do
  has_one :foo
end

resource :quxes do
  has_many :foos
end

freeze_jsonapi

Pretty neat, huh?

Pagination

Sinja::Sequel inspects the first Sequel database to determine whether or not to enable pagination. If (and only if!) you have multiple databases in your application and only some support pagination, you may need to prepend Sinja::Sequel::Pagination after prepending Core, including Helpers, or registering Sinja:

require 'sinja'
require 'sinja/sequel'
require 'sinja/sequel/pagination'

DB = Sequel.connect ENV['DB_URL']

OTHER_DB = Sequel.connect ENV['OTHER_DB_URL']
OTHER_DB.extension :pagination

# Sequel::Model.db now points to DB, which does not support pagination, so
# pagination will not be automatically enabled. We'll point Sinja::Sequel at
# OTHER_DB instead, and manually enable pagination...

class MyApp < Sinatra::Base
  register Sinja
  register Sinja::Sequel

  helpers do
    prepend Sinja::Sequel::Pagination

    def database
      OTHER_DB
    end
  end

  # ..

  freeze_jsonapi
end

Development

After checking out the repo, run bin/setup to install dependencies. Then, run rake spec to run the tests. You can also run bin/console for an interactive prompt that will allow you to experiment.

To install this gem onto your local machine, run bundle exec rake install. To release a new version, update the version number in version.rb, and then run bundle exec rake release, which will create a git tag for the version, push git commits and tags, and push the .gem file to rubygems.org.

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/mwpastore/sinja-sequel.

License

The gem is available as open source under the terms of the MIT License.