-
Notifications
You must be signed in to change notification settings - Fork 57
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Support defining multiple stores with the same store_attribute #93
base: master
Are you sure you want to change the base?
Support defining multiple stores with the same store_attribute #93
Conversation
@byroot This is ready for review |
I'm in vacation this week. I'll try to have a look this week end. |
Ok, so I had a quick look at this, and I don't think it's typed-store job to work around such a problem. I think we should improve this in Active Record, either have a store "inheritance" behavior or either explicitly break when such case happens. |
Just to clarify, the rails store works as intended and store attributes are inherited properly there. I would expect the method # From https://api.rubyonrails.org/classes/ActiveRecord/Store.html
class User
store :settings, accessors: [ :two_factor_auth ]
store :settings, accessors: [ :login_retry ]
end
User.stored_attributes[:settings] # => [:two_factor_auth, :login_retry] But if you do the same with typed_store: class User
typed_store :settings, accessors: [ :two_factor_auth ]
typed_store :settings, accessors: [ :login_retry ]
end
# Typed store:
User.store_accessors # => #<Set: {"login_retry"}>
# Rails:
User.stored_attributes[:settings] # => [:two_factor_auth, :login_retry] |
Ah I see, my bad. I'm ok with fixing this, but your PR is really big and a bit hard to grasp. I think first we should fix the test suite that is failing because of the YAML changes in Rails. For a quick fix we should add all the necessary types to the allow list in the test app. It's best done as a standalone PR, once it's done it will be easier to review. However I super swamped this week (and probably the couple next weeks as well) so I'm not sure I'll have a whole lot of time to look at this. |
def inherited(sub_class) | ||
super(sub_class) | ||
|
||
sub_class.typed_stores = Marshal.load(Marshal.dump(self.typed_stores)) if self.respond_to? :typed_stores |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Marshal.load(Marshal.dump())
isn't acceptable here, and would break if the code contain procs and things like that.
We should dup the necessary things, not rely on a deep_dup hack.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll see if I can find another way to dup the stores, this is to prevent modifying the store in parent class
I'll see if I can find time to fix the CI. I briefly checked it out last night and it seems there are more issues than just the YAML changes. |
I fixed CI this morning. |
b946c2a
to
4e22cf1
Compare
4e22cf1
to
8096380
Compare
self.store_accessors = typed_stores.each_value.flat_map { |d| d.accessors.values }.map { |a| -a.to_s }.to_set | ||
|
||
typed_klass = TypedHash.create(dsl.fields.values) | ||
const_set("#{store_attribute}_hash".camelize, typed_klass) | ||
const_name = "#{store_attribute}_hash".camelize |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you define a store multiple times a warning will be thrown on "already initialized constant"
|
||
def store_accessors(options) | ||
set_affixes(options) | ||
@accessors = if options[:accessors] == false |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've let the accessors be for now, but I think those should also be fixed so they are preserved.
With the fixes in this MR:
class User < ActiveRecord::Base
typed_store(:settings) { |s| s.any :two_factor_auth }
typed_store(:settings) { |s| s.any :login_retry }
end
# This results in:
User.store_accessors # => #<Set: {"two_factor_auth", "login_retry"}>
User.typed_stores[:settings].fields.keys # => [:two_factor_auth, :login_retry]
User.stored_attributes[:settings] # => [:two_factor_auth, :login_retry]
# When using the accessors option:
class User < ActiveRecord::Base
typed_store(:settings, accessors: [:two_factor_auth]) { |s| s.any :two_factor_auth }
typed_store(:settings, accessors: [:login_retry]) { |s| s.any :login_retry }
end
# This results in:
User.store_accessors # => #<Set: {"login_retry"}>
User.typed_stores[:settings].fields.keys # => [:two_factor_auth, :login_retry]
User.stored_attributes[:settings] # => [:two_factor_auth, :login_retry]
I propose that the logic for the accessors should be:
accessors: false
option should only disable accessors for the fields in current store- You should only be allowed to define accessors that are defined in the store. (why would you define an accessor that will have no value?)
E.g.typed_store(:settings, accessors: [:key_1, :key_2]) { |s| s.any :key_1 }
. makes no sense as key_2 is not defined - If defining more than one store on the same store attribute the accessors should be merged.
This of course can be fixed after this fix or when someone asks for it
# Copy the store to the sub class to avoid mutation of the store in parent class | ||
sub_class.typed_stores = self.typed_stores.map do |store_attribute, store| | ||
new_store = store.dup | ||
new_store.instance_variable_set(:'@fields', store.fields.dup) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Alternative could be to make fields an accessor, but up to you what you prefer here
We are currently using StoreExt in our projects which is now archived and not maintained anymore. Upon migrating one of our projects I discovered one case that is not supported here. I also came across #36 which also touches the same issue.
In some of our models we have defined a store with fields specific for that model and also included a store that is common among other models.
F.ex
The example above results in a redefinition of the store constant and the class variable
store_accessors
contain only the last defined store. You can still access the all the fields via the accessors, but you lose the typed store for the first defined store.This PR solves this by: