JDs Work

A blog about technology, careers, and personal growth.

Thoughts on Interfaces for Models

February 11, 2021
Category: 
Development

100 Days to Offload Challenge

This is post 2 as part of the #100DaysToOffload challenge. The point is to write 100 posts on a personal blog in a year. Quality isn't as important as quantity so some posts may be a little messy. Read other posts in this challenge.

I recently had to build an interesting model that stored values for a JWT in order to implement an allow list style revocation strategy1. After some feedback from another developer it became clear the interface for that model needed to be optimized. Here's a quick description of the "behavior" of that model:

  • All of the columns are read only after creation
  • It's dependent on a User record assocation - thus requires a validation
  • It has an expiration time that is also stored, but set to a pre-determined amount of time
  • It's jti column value is generated by the model itself since it is a "propietary" action per record

Given this set of behavior we can infer that since the expires_at column and jti are both self generated in the model code, the only attribute required for creation is the associated User record.

This made the code for the model drastically simpler and also gave me constraints to artificially impose on the model itself, preventing updates and making attributes read only.

Rails provides a nifty way of doing these things but this principal can be used with any language/framework.

# Model Class Example
class AllowListedToken < ApplicationRecord

  # ...
  attr_readonly :jti, :user_id, :expires_at # prevents update calls on these columns

  EXPIRATION_TIME = 1.day.from_now

  belongs_to :user

  ## after_initialize is called when the object is created but before the `INSERT` is called
  ## allowing for object transformations to take place before the record persists.
  after_initialize :set_generated_values
  
  # ...

  private

  def set_generated_values
    self.jti = JtiGenerator.new.jti
    self.expires_at = EXPIRATION_TIME
  end
end

# Usage
user = User.find(id)
AllowListedToken.create!(user: user)

The moral of the story is to take time to consider how your model should behave and what limitations or defaults you can implement to ensure that the constraints you need to fulfill are fulfilled. This helps ensure the maintainability and simplicity of the model and helps to align the expectated behavior and usage.


Definitions

[1] Allow List Revocation Strategy: This is a token revocation strategy that uses stored values to validate that a token is valid. Look ups for the token are used to ensure it's valid. If a token is not found in the table, then authentication fails. Revoking is essentially destroying a record in the token table. Devise based resource.