DEV Community

loading...
Cover image for JSONAPI::Resources & non-default relationship names

JSONAPI::Resources & non-default relationship names

Michal Bryxí
Cycle 🚴 , climb 🗻 , run 🏃 , travel 🌍 , enjoy life ♥. IT guy with the need to live fully.
Updated on ・2 min read

JSONAPI is a standard that serves as an API anti-bikeshedding tool. JSONAPI::Resources is a minimal coding implementation of said standard for Ruby on Rails(RoR) and helped me to build big & complex codebases with next to zero lines of code.

JSONAPI::Resources builds on standards that RoR comes with, so in most cases you don't have to add any extra code and it will just work™. But every time I have to override default relationship names I get lost in the documentation. So I'm going to document my recent example here for future me (and you):

First we need to define two resources. For this example I will use:

  • Two models: Meeting and Election.
  • Where meeting can have one active_election.
  • Therefore in table meetings there is a column active_election_id.

Show me the code

Let's see how the schema looks like:

# backend/db/schema.rb

ActiveRecord::Schema.define(version: 2020_11_21_090004) do
  create_table "meetings", force: :cascade do |t|
    t.bigint "active_election_id"
    # some other attributes
  end

  create_table "elections", force: :cascade do |t|
    # some other attributes
  end
Enter fullscreen mode Exit fullscreen mode

Then we will need to define routes:

# backend/config/routes.rb

Rails.application.routes.draw do
  namespace :api do
    jsonapi_resources :elections
    jsonapi_resources :meetings
  end
end
Enter fullscreen mode Exit fullscreen mode

models:

# backend/app/models/meeting.rb
class Meeting < ApplicationRecord
  belongs_to :active_election, class_name: 'Election', optional: true
end

# backend/app/models/election.rb
class Election < ApplicationRecord
  has_one :active_for, class_name: 'Meeting', foreign_key: 'active_election_id'
end
Enter fullscreen mode Exit fullscreen mode

and finally resources:

# backend/app/resources/api/meeting_resource.rb
module Api
  class MeetingResource < Api::BaseResource
    model_name 'Meeting'

    has_one :active_election
  end
end

# backend/app/resources/api/election_resource.rb
module Api
  class ElectionResource < Api::BaseResource
    model_name 'Election'

    has_one :active_for, class_name: 'Meeting', foreign_key: 'active_election_id'
  end
end
Enter fullscreen mode Exit fullscreen mode

Note: Api::BaseResource is not strictly necessary, but it's an abstraction that I find very useful, so just to make the example complete:

# backend/app/resources/api/base_resource.rb
module Api
  class BaseResource < JSONAPI::Resource
    abstract
    # All the stuff that's common to all resources.
    # For example pundit authorization:
    include JSONAPI::Authorization::PunditScopedResource
  end
end
Enter fullscreen mode Exit fullscreen mode

How to use this in EmberJS

API is one part of the story. But I still have to have a client that will use it. In my case it's EmberJS and ember-data:

// ui/frontend/app/models/meeting.js
import Model, { belongsTo } from '@ember-data/model';

export default class MeetingModel extends Model {
  @belongsTo('election', { inverse: "activeFor" }) activeElection;
}

// ui/frontend/app/models/election.js
import Model, { hasMany } from '@ember-data/model';

export default class ElectionModel extends Model {
  @hasMany('meeting', {inverse: 'activeElection'}) activeFor;
}
Enter fullscreen mode Exit fullscreen mode

Conclusion

At this point I can simply assign active_election to a meeting:

let meeting = this.store.find('meeting', 27);
let election = this.store.find('election', 42);

meeting.activeElection = election;
meeting.save();
Enter fullscreen mode Exit fullscreen mode

And it should all work 🪄🦄

Notes

  • In these cases the usage of hasMany and belongsTo might be confusing, but it's because those just define where the foreign_key lives. And partly because of my poor naming conventions.
  • There might be a way to simplify this even more and I will try to keep the article updated as I find orientate myself more in the topic.

Photo by Startup Stock Photos from Pexels

Discussion (0)