rolify 4.1.1 → 6.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (55) hide show
  1. checksums.yaml +5 -5
  2. data/.github/workflows/activerecord.yml +27 -0
  3. data/.github/workflows/mongoid.yml +32 -0
  4. data/.hakiri.yml +1 -0
  5. data/Appraisals +42 -0
  6. data/CHANGELOG.rdoc +52 -8
  7. data/CONTRIBUTORS +11 -0
  8. data/Gemfile +6 -14
  9. data/LICENSE +1 -1
  10. data/README.md +72 -26
  11. data/Rakefile +6 -1
  12. data/gemfiles/activerecord_4.gemfile +20 -0
  13. data/gemfiles/activerecord_5.gemfile +22 -0
  14. data/gemfiles/activerecord_6.gemfile +22 -0
  15. data/gemfiles/mongoid_5.gemfile +20 -0
  16. data/gemfiles/mongoid_6.gemfile +19 -0
  17. data/gemfiles/mongoid_7.gemfile +20 -0
  18. data/lib/generators/active_record/rolify_generator.rb +45 -11
  19. data/lib/generators/active_record/templates/migration.rb +2 -3
  20. data/lib/generators/active_record/templates/model.rb +15 -0
  21. data/lib/generators/rolify/templates/initializer.rb +4 -1
  22. data/lib/rolify.rb +3 -4
  23. data/lib/rolify/adapters/active_record/resource_adapter.rb +14 -8
  24. data/lib/rolify/adapters/active_record/role_adapter.rb +40 -17
  25. data/lib/rolify/adapters/mongoid/role_adapter.rb +38 -8
  26. data/lib/rolify/configure.rb +4 -2
  27. data/lib/rolify/dynamic.rb +2 -16
  28. data/lib/rolify/finders.rb +6 -1
  29. data/lib/rolify/matchers.rb +2 -2
  30. data/lib/rolify/resource.rb +1 -1
  31. data/lib/rolify/role.rb +11 -2
  32. data/lib/rolify/version.rb +1 -1
  33. data/rolify.gemspec +15 -7
  34. data/spec/common_helper.rb +16 -0
  35. data/spec/generators/rolify/rolify_activerecord_generator_spec.rb +112 -6
  36. data/spec/generators_helper.rb +9 -2
  37. data/spec/rolify/config_spec.rb +2 -0
  38. data/spec/rolify/custom_spec.rb +1 -1
  39. data/spec/rolify/resource_spec.rb +6 -0
  40. data/spec/rolify/shared_examples/shared_examples_for_finders.rb +50 -32
  41. data/spec/rolify/shared_examples/shared_examples_for_has_role.rb +65 -0
  42. data/spec/rolify/utils_spec.rb +19 -0
  43. data/spec/spec_helper.rb +19 -6
  44. data/spec/support/adapters/active_record.rb +4 -3
  45. data/spec/support/adapters/mongoid.rb +19 -2
  46. data/spec/support/adapters/{mongoid.yml → mongoid_5.yml} +2 -2
  47. data/spec/support/adapters/mongoid_6.yml +6 -0
  48. data/spec/support/adapters/mongoid_7.yml +6 -0
  49. data/spec/support/adapters/utils/active_record.rb +12 -0
  50. data/spec/support/adapters/utils/mongoid.rb +13 -0
  51. metadata +60 -26
  52. data/.travis.yml +0 -25
  53. data/gemfiles/Gemfile.rails-3.2 +0 -27
  54. data/gemfiles/Gemfile.rails-4.0 +0 -33
  55. data/gemfiles/Gemfile.rails-4.1 +0 -37
data/Rakefile CHANGED
@@ -1,6 +1,7 @@
1
1
  require 'bundler'
2
2
  require 'rspec/core/rake_task'
3
3
  require 'coveralls/rake/task'
4
+ require 'appraisal'
4
5
 
5
6
  Bundler::GemHelper.install_tasks
6
7
 
@@ -14,7 +15,11 @@ RSpec::Core::RakeTask.new(:rolify) do |task|
14
15
  task.pattern = 'spec/rolify/**/*_spec.rb'
15
16
  end
16
17
 
17
- task :default => [ :spec, 'coveralls:push' ]
18
+ if !ENV["APPRAISAL_INITIALIZED"] && !ENV["CI"]
19
+ task :default => :appraisal
20
+ else
21
+ task :default => [ :spec, 'coveralls:push' ]
22
+ end
18
23
 
19
24
  desc 'Run all specs'
20
25
  task 'spec' do
@@ -0,0 +1,20 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "sqlite3", "~> 1.3.6"
6
+ gem "activerecord", "~> 4.2.11", require: "active_record"
7
+ gem "bigdecimal", "1.4.2"
8
+
9
+ group :test do
10
+ gem "codeclimate-test-reporter", require: nil
11
+ gem "coveralls", require: false
12
+ gem "database_cleaner", "~> 1.6.2"
13
+ gem "its"
14
+ gem "test-unit"
15
+ gem "byebug"
16
+ gem "pry"
17
+ gem "pry-byebug"
18
+ end
19
+
20
+ gemspec path: "../"
@@ -0,0 +1,22 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "sqlite3", "~> 1.3.6"
6
+ gem "activerecord", "~> 5.2.4", require: "active_record"
7
+ gem "actionpack", "~> 5.2.4"
8
+ gem "activemodel", "~> 5.2.4"
9
+ gem "railties", "~> 5.2.4"
10
+
11
+ group :test do
12
+ gem "codeclimate-test-reporter", require: nil
13
+ gem "coveralls", require: false
14
+ gem "database_cleaner", "~> 1.6.2"
15
+ gem "its"
16
+ gem "test-unit"
17
+ gem "byebug"
18
+ gem "pry"
19
+ gem "pry-byebug"
20
+ end
21
+
22
+ gemspec path: "../"
@@ -0,0 +1,22 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "sqlite3", "~> 1.4", platform: "ruby"
6
+ gem "activerecord", ">= 6.0.0", require: "active_record"
7
+ gem "actionpack", ">= 6.0.0"
8
+ gem "activemodel", ">= 6.0.0"
9
+ gem "railties", ">= 6.0.0"
10
+
11
+ group :test do
12
+ gem "codeclimate-test-reporter", require: nil
13
+ gem "coveralls", require: false
14
+ gem "database_cleaner", "~> 1.6.2"
15
+ gem "its"
16
+ gem "test-unit"
17
+ gem "byebug"
18
+ gem "pry"
19
+ gem "pry-byebug"
20
+ end
21
+
22
+ gemspec path: "../"
@@ -0,0 +1,20 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "mongoid", "~> 5"
6
+ gem "bson_ext", "1.5.1"
7
+ gem "bigdecimal", "1.4.2"
8
+
9
+ group :test do
10
+ gem "codeclimate-test-reporter", require: nil
11
+ gem "coveralls", require: false
12
+ gem "database_cleaner", "~> 1.6.2"
13
+ gem "its"
14
+ gem "test-unit"
15
+ gem "byebug"
16
+ gem "pry"
17
+ gem "pry-byebug"
18
+ end
19
+
20
+ gemspec path: "../"
@@ -0,0 +1,19 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "mongoid", "~> 6"
6
+ gem "bson_ext", "1.5.1"
7
+
8
+ group :test do
9
+ gem "codeclimate-test-reporter", require: nil
10
+ gem "coveralls", require: false
11
+ gem "database_cleaner", "~> 1.6.2"
12
+ gem "its"
13
+ gem "test-unit"
14
+ gem "byebug"
15
+ gem "pry"
16
+ gem "pry-byebug"
17
+ end
18
+
19
+ gemspec path: "../"
@@ -0,0 +1,20 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "mongoid", "~> 7"
6
+ gem "bson_ext", "1.5.1"
7
+ gem "railties", "5.2.4.1"
8
+
9
+ group :test do
10
+ gem "codeclimate-test-reporter", require: nil
11
+ gem "coveralls", require: false
12
+ gem "database_cleaner", "~> 1.6.2"
13
+ gem "its"
14
+ gem "test-unit"
15
+ gem "byebug"
16
+ gem "pry"
17
+ gem "pry-byebug"
18
+ end
19
+
20
+ gemspec path: "../"
@@ -1,5 +1,6 @@
1
1
  require 'rails/generators/active_record'
2
2
  require 'active_support/core_ext'
3
+ require 'erb'
3
4
 
4
5
  module ActiveRecord
5
6
  module Generators
@@ -8,6 +9,13 @@ module ActiveRecord
8
9
 
9
10
  argument :user_cname, :type => :string, :default => "User", :banner => "User"
10
11
 
12
+ def ensure_user_class_defined
13
+ unless user_class_defined?
14
+ prompt_missing_user
15
+ abort
16
+ end
17
+ end
18
+
11
19
  def generate_model
12
20
  invoke "active_record:model", [ name ], :migration => false
13
21
  end
@@ -22,15 +30,18 @@ module ActiveRecord
22
30
  require "#{ENGINE_ROOT}/app/models/#{user_cname.downcase}.rb"
23
31
  end
24
32
  end
33
+
25
34
  inject_into_class(model_path, class_name, model_content)
26
35
  end
27
36
 
28
37
  def copy_rolify_migration
29
- migration_template "migration.rb", "db/migrate/rolify_create_#{table_name}.rb"
38
+ migration_template "migration.rb", "db/migrate/rolify_create_#{table_name}.rb", migration_version: migration_version
30
39
  end
31
40
 
41
+ private
42
+
32
43
  def join_table
33
- user_cname.constantize.table_name + "_" + table_name
44
+ user_class.table_name + "_" + table_name
34
45
  end
35
46
 
36
47
  def user_reference
@@ -46,18 +57,41 @@ module ActiveRecord
46
57
  end
47
58
 
48
59
  def model_content
49
- content = <<RUBY
50
- has_and_belongs_to_many :%{user_cname}, :join_table => :%{join_table}
51
- belongs_to :resource, :polymorphic => true
60
+ ERB.new(File.read(File.join(__dir__, 'templates/model.rb'))).result(binding)
61
+ end
62
+
63
+ def user_class
64
+ user_cname.constantize
65
+ end
52
66
 
53
- validates :resource_type,
54
- :inclusion => { :in => Rolify.resource_types },
55
- :allow_nil => true
67
+ def user_class_defined?
68
+ user_class
69
+ true
70
+ rescue NameError => ex
71
+ if ex.missing_name == user_cname
72
+ false
73
+ else
74
+ raise ex
75
+ end
76
+ end
77
+
78
+ def prompt_missing_user
79
+ puts <<MSG
80
+ Rolify expected a model named #{user_cname} to be defined but could not find one.
81
+ Please ensure that this model exists and is not mis-spelled and re-run the generator.
82
+ MSG
83
+ end
56
84
 
57
- scopify
58
- RUBY
59
- content % { :user_cname => user_cname.constantize.table_name, :join_table => "#{user_cname.constantize.table_name}_#{table_name}"}
85
+ def versioned_migrations?
86
+ Rails::VERSION::MAJOR >= 5
60
87
  end
88
+
89
+ def migration_version
90
+ if versioned_migrations?
91
+ "[#{Rails::VERSION::MAJOR}.#{Rails::VERSION::MINOR}]"
92
+ end
93
+ end
94
+
61
95
  end
62
96
  end
63
97
  end
@@ -1,4 +1,4 @@
1
- class RolifyCreate<%= table_name.camelize %> < ActiveRecord::Migration
1
+ class RolifyCreate<%= table_name.camelize %> < ActiveRecord::Migration<%= migration_version %>
2
2
  def change
3
3
  create_table(:<%= table_name %>) do |t|
4
4
  t.string :name
@@ -11,8 +11,7 @@ class RolifyCreate<%= table_name.camelize %> < ActiveRecord::Migration
11
11
  t.references :<%= user_reference %>
12
12
  t.references :<%= role_reference %>
13
13
  end
14
-
15
- add_index(:<%= table_name %>, :name)
14
+ <% if ActiveRecord::Base.connection.class.to_s.demodulize != 'PostgreSQLAdapter' %><%= "\n " %>add_index(:<%= table_name %>, :name)<% end %>
16
15
  add_index(:<%= table_name %>, [ :name, :resource_type, :resource_id ])
17
16
  add_index(:<%= join_table %>, [ :<%= user_reference %>_id, :<%= role_reference %>_id ])
18
17
  end
@@ -0,0 +1,15 @@
1
+ has_and_belongs_to_many :<%= user_class.table_name %>, :join_table => :<%= join_table %>
2
+ <% if Rails::VERSION::MAJOR < 5 %>
3
+ belongs_to :resource,
4
+ :polymorphic => true
5
+ <% else %>
6
+ belongs_to :resource,
7
+ :polymorphic => true,
8
+ :optional => true
9
+ <% end %>
10
+
11
+ validates :resource_type,
12
+ :inclusion => { :in => Rolify.resource_types },
13
+ :allow_nil => true
14
+
15
+ scopify
@@ -4,4 +4,7 @@ Rolify.configure<%= "(\"#{class_name.camelize.to_s}\")" if class_name != "Role"
4
4
 
5
5
  # Dynamic shortcuts for User class (user.is_admin? like methods). Default is: false
6
6
  # config.use_dynamic_shortcuts
7
- end
7
+
8
+ # Configuration to remove roles from database once the last resource is removed. Default is: true
9
+ # config.remove_role_if_empty = false
10
+ end
data/lib/rolify.rb CHANGED
@@ -25,12 +25,11 @@ module Rolify
25
25
 
26
26
  rolify_options = { :class_name => options[:role_cname].camelize }
27
27
  rolify_options.merge!({ :join_table => self.role_join_table_name }) if Rolify.orm == "active_record"
28
- rolify_options.merge!(options.reject{ |k,v| ![ :before_add, :after_add, :before_remove, :after_remove ].include? k.to_sym })
28
+ rolify_options.merge!(options.reject{ |k,v| ![ :before_add, :after_add, :before_remove, :after_remove, :inverse_of ].include? k.to_sym })
29
29
 
30
- has_and_belongs_to_many :roles, rolify_options
30
+ has_and_belongs_to_many :roles, **rolify_options
31
31
 
32
32
  self.adapter = Rolify::Adapter::Base.create("role_adapter", self.role_cname, self.name)
33
- load_dynamic_methods if Rolify.dynamic_shortcuts
34
33
 
35
34
  #use strict roles
36
35
  self.strict_rolify = true if options[:strict]
@@ -49,7 +48,7 @@ module Rolify
49
48
  self.role_cname = options[:role_cname]
50
49
  self.role_table_name = self.role_cname.tableize.gsub(/\//, "_")
51
50
 
52
- has_many association_name, resourcify_options
51
+ has_many association_name, **resourcify_options
53
52
 
54
53
  self.resource_adapter = Rolify::Adapter::Base.create("resource_adapter", self.role_cname, self.name)
55
54
  @@resource_types << self.name
@@ -17,16 +17,17 @@ module Rolify
17
17
  str << ', ' unless klass == klasses.last
18
18
  str
19
19
  end
20
- resources = relation.joins("INNER JOIN #{quote(roles_table)} ON #{quote(roles_table)}.resource_type IN (#{relations}) AND
21
- (#{quote(roles_table)}.resource_id IS NULL OR #{quote(roles_table)}.resource_id = #{quote(relation.table_name)}.#{relation.primary_key})")
22
- resources = resources.where("#{quote(roles_table)}.name IN (?) AND #{quote(roles_table)}.resource_type IN (?)", Array(role_name), klasses)
23
- resources = resources.select("#{quote(relation.table_name)}.*")
20
+
21
+ resources = relation.joins("INNER JOIN #{quote_table(roles_table)} ON #{quote_table(roles_table)}.resource_type IN (#{relations}) AND
22
+ (#{quote_table(roles_table)}.resource_id IS NULL OR #{quote_table(roles_table)}.resource_id = #{quote_table(relation.table_name)}.#{quote_column(relation.primary_key)})")
23
+ resources = resources.where("#{quote_table(roles_table)}.name IN (?) AND #{quote_table(roles_table)}.resource_type IN (?)", Array(role_name), klasses)
24
+ resources = resources.select("#{quote_table(relation.table_name)}.*")
24
25
  resources
25
26
  end
26
27
 
27
28
  def in(relation, user, role_names)
28
- roles = user.roles.where(:name => role_names).select("#{quote(role_class.table_name)}.#{role_class.primary_key}")
29
- relation.where("#{quote(role_class.table_name)}.#{role_class.primary_key} IN (?) AND ((resource_id = #{quote(relation.table_name)}.#{relation.primary_key}) OR (resource_id IS NULL))", roles)
29
+ roles = user.roles.where(:name => role_names).select("#{quote_table(role_class.table_name)}.#{quote_column(role_class.primary_key)}")
30
+ relation.where("#{quote_table(role_class.table_name)}.#{quote_column(role_class.primary_key)} IN (?) AND ((#{quote_table(role_class.table_name)}.resource_id = #{quote_table(relation.table_name)}.#{quote_column(relation.primary_key)}) OR (#{quote_table(role_class.table_name)}.resource_id IS NULL))", roles)
30
31
  end
31
32
 
32
33
  def applied_roles(relation, children)
@@ -39,14 +40,19 @@ module Rolify
39
40
 
40
41
  def all_except(resource, excluded_obj)
41
42
  prime_key = resource.primary_key.to_sym
42
- resource.where(prime_key => (resource.all - excluded_obj).map(&prime_key))
43
+ resource.where.not(prime_key => excluded_obj.pluck(prime_key))
43
44
  end
44
45
 
45
46
  private
46
47
 
47
- def quote(column)
48
+ def quote_column(column)
48
49
  ActiveRecord::Base.connection.quote_column_name column
49
50
  end
51
+
52
+ def quote_table(table)
53
+ ActiveRecord::Base.connection.quote_table_name table
54
+ end
55
+
50
56
  end
51
57
  end
52
58
  end
@@ -9,13 +9,42 @@ module Rolify
9
9
  end
10
10
 
11
11
  def where_strict(relation, args)
12
- resource = if args[:resource].is_a?(Class)
13
- {class: args[:resource].to_s, id: nil}
14
- else
15
- {class: args[:resource].class.name, id: args[:resource].id}
16
- end
12
+ wrap_conditions = relation.name != role_class.name
17
13
 
18
- relation.where(:name => args[:name], :resource_type => resource[:class], :resource_id => resource[:id])
14
+ conditions = if args[:resource].is_a?(Class)
15
+ {:resource_type => args[:resource].to_s, :resource_id => nil }
16
+ elsif args[:resource].present?
17
+ {:resource_type => args[:resource].class.name, :resource_id => args[:resource].id}
18
+ else
19
+ {}
20
+ end
21
+
22
+ conditions.merge!(:name => args[:name])
23
+ conditions = wrap_conditions ? { role_table => conditions } : conditions
24
+
25
+ relation.where(conditions)
26
+ end
27
+
28
+ def find_cached(relation, args)
29
+ resource_id = (args[:resource].nil? || args[:resource].is_a?(Class) || args[:resource] == :any) ? nil : args[:resource].id
30
+ resource_type = args[:resource].is_a?(Class) ? args[:resource].to_s : args[:resource].class.name
31
+
32
+ return relation.find_all { |role| role.name == args[:name].to_s } if args[:resource] == :any
33
+
34
+ relation.find_all do |role|
35
+ (role.name == args[:name].to_s && role.resource_type == nil && role.resource_id == nil) ||
36
+ (role.name == args[:name].to_s && role.resource_type == resource_type && role.resource_id == nil) ||
37
+ (role.name == args[:name].to_s && role.resource_type == resource_type && role.resource_id == resource_id)
38
+ end
39
+ end
40
+
41
+ def find_cached_strict(relation, args)
42
+ resource_id = (args[:resource].nil? || args[:resource].is_a?(Class)) ? nil : args[:resource].id
43
+ resource_type = args[:resource].is_a?(Class) ? args[:resource].to_s : args[:resource].class.name
44
+
45
+ relation.find_all do |role|
46
+ role.resource_id == resource_id && role.resource_type == resource_type && role.name == args[:name].to_s
47
+ end
19
48
  end
20
49
 
21
50
  def find_or_create_by(role_name, resource_type = nil, resource_id = nil)
@@ -23,7 +52,7 @@ module Rolify
23
52
  end
24
53
 
25
54
  def add(relation, role)
26
- relation.role_ids |= [role.id]
55
+ relation.roles << role unless relation.roles.include?(role)
27
56
  end
28
57
 
29
58
  def remove(relation, role_name, resource = nil)
@@ -44,20 +73,14 @@ module Rolify
44
73
  relation.where("#{column} IS NOT NULL")
45
74
  end
46
75
 
47
- def scope(relation, conditions)
48
- if Rails.version < "4.0"
49
- query = relation.scoped
50
- else
51
- query = relation.all
52
- end
53
- query = query.joins(:roles)
54
- query = where(query, conditions)
76
+ def scope(relation, conditions, strict)
77
+ query = relation.joins(:roles)
78
+ query = strict ? where_strict(query, conditions) : where(query, conditions)
55
79
  query
56
80
  end
57
81
 
58
82
  def all_except(user, excluded_obj)
59
- prime_key = user.primary_key.to_sym
60
- user.where(prime_key => (user.all - excluded_obj).map(&prime_key))
83
+ user.where.not(user.primary_key => excluded_obj)
61
84
  end
62
85
 
63
86
  private
@@ -9,13 +9,42 @@ module Rolify
9
9
  end
10
10
 
11
11
  def where_strict(relation, args)
12
- resource = if args[:resource].is_a?(Class)
13
- {class: args[:resource].to_s, id: nil}
14
- else
15
- {class: args[:resource].class.name, id: args[:resource].id}
16
- end
12
+ wrap_conditions = relation.name != role_class.name
17
13
 
18
- relation.where(:name => args[:name], :resource_type => resource[:class], :resource_id => resource[:id])
14
+ conditions = if args[:resource].is_a?(Class)
15
+ {:resource_type => args[:resource].to_s, :resource_id => nil }
16
+ elsif args[:resource].present?
17
+ {:resource_type => args[:resource].class.name, :resource_id => args[:resource].id}
18
+ else
19
+ {}
20
+ end
21
+
22
+ conditions.merge!(:name => args[:name])
23
+ conditions = wrap_conditions ? { role_table => conditions } : conditions
24
+
25
+ relation.where(conditions)
26
+ end
27
+
28
+ def find_cached(relation, args)
29
+ resource_id = (args[:resource].nil? || args[:resource].is_a?(Class) || args[:resource] == :any) ? nil : args[:resource].id
30
+ resource_type = args[:resource].is_a?(Class) ? args[:resource].to_s : args[:resource].class.name
31
+
32
+ return relation.find_all { |role| role.name == args[:name].to_s } if args[:resource] == :any
33
+
34
+ relation.find_all do |role|
35
+ (role.name == args[:name].to_s && role.resource_type == nil && role.resource_id == nil) ||
36
+ (role.name == args[:name].to_s && role.resource_type == resource_type && role.resource_id == nil) ||
37
+ (role.name == args[:name].to_s && role.resource_type == resource_type && role.resource_id == resource_id)
38
+ end
39
+ end
40
+
41
+ def find_cached_strict(relation, args)
42
+ resource_id = (args[:resource].nil? || args[:resource].is_a?(Class)) ? nil : args[:resource].id
43
+ resource_type = args[:resource].is_a?(Class) ? args[:resource].to_s : args[:resource].class.name
44
+
45
+ relation.find_all do |role|
46
+ role.resource_id == resource_id && role.resource_type == resource_type && role.name == args[:name].to_s
47
+ end
19
48
  end
20
49
 
21
50
  def find_or_create_by(role_name, resource_type = nil, resource_id = nil)
@@ -61,8 +90,9 @@ module Rolify
61
90
  relation.where(column.to_sym.ne => nil)
62
91
  end
63
92
 
64
- def scope(relation, conditions)
65
- roles = where(role_class, conditions).map { |role| role.id }
93
+ def scope(relation, conditions, strict)
94
+ query = strict ? where_strict(role_class, conditions) : where(role_class, conditions)
95
+ roles = query.map { |role| role.id }
66
96
  return [] if roles.size.zero?
67
97
  query = relation.any_in(:role_ids => roles)
68
98
  query