Defining attributes at runtime based on data from related object

208 views Asked by At

I'm building an application where users are part of an Organisation. An organisation has many Lists, which in turn have many ListItems.

Now, I would like for admin users to be able to specify which attributes are available on list items, based on the organisation they belong to (or rather, on the organisation their list belongs to), without having to touch any code.

So far, when defining attributes that are not bound to a specific column in the database, I have used document_serializable, a nifty little gem (based on virtus) which serializes virtual attributes to a JSONB column in the db. I like this approach, because I get all of virtus' goodies (types, coercion, validations, etc.), and because data ends up sitting in a JSONB column, meaning it can be loaded quickly, indexed, and searched through with relative ease.

I would like to keep using this approach when adding these user-defined attributes on the fly. So I'd like to do something like:

class ListItem < ApplicationRecord
  belongs_to :list
  delegate :organisation, to: :list

  organisation.list_attributes.each do |a, t|
    attribute a, t
  end
end

Where Organisation#list_attributes returns the user-defined hash of attribute names and their associated types, which, for example, might look like:

{
  name: String,
  age: Integer
}

As you might have guessed, this does not work, because organisation.list_attributes.each actually runs in the context of ListItem, which is an instance of Class, and Class doesn't have an #organisation method. I hope that's worded in a way that makes sense1.

I've tried using after_initialize, but at that point in the object's lifecycle, #attribute is owned by ActiveRecord::AttributeMethods::Read and not DocumentSerializable::ClassMethods, so it's an entirely different method and I can't figure out wether I can still access the one I need, and wether that would even work.

Another alternative would be to find the organisation in question in some explicit way, Organisation#find-style, but I honestly don't know where I should store the information necessary to do so.

So, my question: at the moment of instantiating (initializing or loading2) a record, is there a way I can retrieve a hash stored in a database column of one of its relations? Or am I trying to build this in a completely misguided way, and if so, how else should I go about it?


1 To clarify, if I were to use the hash directly like so:

class ListItem < ApplicationRecord
  belongs_to :list
  delegate :organisation, to: :list

  {
    name: String,
    age: Integer
  }.each do |a, t|
    attribute a, t
  end
end

it would work, my issue is solely with getting a record's relation at this earlier point in time.

2 My understanding is that Rails runs a model's code whenever a record of that type is created or loaded from the database, meaning the virtual attributes are defined anew every time this happens, which is why I'm asking how to do this in both cases.

1

There are 1 answers

1
Veridian Dynamics On

at the moment of instantiating (initializing or loading) a record, is there a way I can retrieve a hash stored in a database column of one of its relations?

Yes. This is fairly trivial as long as your relations are setup correctly / simply. Lets say we have these three models:

class ListItem < ApplicationRecord
  belongs_to :list
end

class List < ApplicationRecord
  belongs_to :organisation
  has_many :list_items
end

class Organisation < ApplicationRecord
  has_many :lists
end

We can instantiate a ListItem and then retrieve data from anyone of its parents.

@list_item = ListItem.find(5) # assume that the proper inherited 
                                foreign_keys exist for this and 
                                its parent
@list = @list_item.list
@hash = @list.organisation.special_hash_of_org

And if we wanted to do this at every instance of a ListItem, we can use Active Record Callbacks like this:

class ListItem < ApplicationRecord
  belongs_to :list

  # this is called on ListItem.new and whenever we pull from our DB
  after_initialize do |list_item|
    puts "You have initialized a ListItem!"
    list = list_item.list
    hash = list.organisation.special_hash_of_org
  end

end

But after_initialize feels like a strange usage for this kind of thing. Maybe a helper method would be a better option!