flipper-active_record 1.3.0 → 1.3.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: cd8f5a24abcc3c82a2335e17b796fa84721522eb92bf138ebab4146b58da7fdc
4
- data.tar.gz: c39a1899083f55078e7ca28e23d8b1363609ca1322be6d4f32cfcd23922440e2
3
+ metadata.gz: 6db1458b33ebce656ac2fd6a0aadeb5e2547b5cd346644ec64cf54034f93a461
4
+ data.tar.gz: ab9c8dee420bef036736f998a99411a89c70cdc794cdb96ec0af355a5a682902
5
5
  SHA512:
6
- metadata.gz: 16b65bea23bf718930e3509b47ba4719ba4baf93dd86bccf69b292130a7732eff1ab7ea9843c9d4a7342e8180cafd08513bcdd14997be5189e6923afe575e499
7
- data.tar.gz: '01816a252d70e1a9c0bd7215893c54f6ac5dd9444cf9c86b571455edb9b45b0b6bae6072a1e678d283eb70c067a738071a7b7625ace489464dda551574a36121'
6
+ metadata.gz: 4f8a73e94ebfdb0b1547ec276880c3c57e1b79320036af3f75c34bf6e8a1743b3a245351f5eaed5f4f4ed643a9bec6c8cec8954d1e2ac91ae71660b00c5a079d
7
+ data.tar.gz: a39fbf5b9517806fbaf22e303663ddd33e0a974d7e9913f5793c4ac3ef74cf9aa23782d4dacdad256b672c3a4dcc65b12aabb8520c30490147bf10449039631b
@@ -28,3 +28,18 @@ SQL
28
28
  ActiveRecord::Base.connection.execute <<-SQL
29
29
  CREATE UNIQUE INDEX index_gates_on_keys_and_value on flipper_gates (feature_key, key, value)
30
30
  SQL
31
+
32
+ ActiveRecord::Base.connection.execute <<-SQL
33
+ CREATE TABLE users (
34
+ id integer PRIMARY KEY,
35
+ name string NOT NULL,
36
+ influencer boolean,
37
+ created_at datetime NOT NULL,
38
+ updated_at datetime NOT NULL
39
+ )
40
+ SQL
41
+
42
+ require 'flipper/model/active_record'
43
+ class User < ActiveRecord::Base
44
+ include Flipper::Model::ActiveRecord
45
+ end
@@ -0,0 +1,68 @@
1
+ # This is an example script that shows how to migrate from a bunch of individual
2
+ # actors to a group. Should be useful for those who have ended up with large
3
+ # actor sets and want to slim them down for performance reasons.
4
+
5
+ require_relative "./ar_setup"
6
+ require 'flipper/adapters/active_record'
7
+ require 'active_support/all'
8
+
9
+ # 1. enable feature for 100 actors, make 80 influencers
10
+ users = 100.times.map do |n|
11
+ influencer = n < 80 ? true : false
12
+ user = User.create(name: n, influencer: influencer)
13
+ Flipper.enable :stats, user
14
+ user
15
+ end
16
+
17
+ # check enabled, should all be because individual actors are enabled
18
+ print 'Should be [[true, 100]]: '
19
+ print users.group_by { |user| Flipper.enabled?(:stats, user) }.map { |result, users| [result, users.size]}
20
+ puts
21
+
22
+ # 2. register a group so flipper knows what to do with it
23
+ Flipper.register(:influencers) do |actor, context|
24
+ actor.respond_to?(:influencer) && actor.influencer
25
+ end
26
+
27
+ # 3. enable group for feature, THIS IS IMPORTANT
28
+ Flipper.enable :stats, :influencers
29
+
30
+ # check enabled again, should all still be true because individual actors are
31
+ # enabled, but also the group gate would return true for 80 influencers. At this
32
+ # point, it's kind of double true but flipper just cares if any gate returns true.
33
+ print 'Should be [[true, 100]]: '
34
+ print users.group_by { |user| Flipper.enabled?(:stats, user) }.map { |result, users| [result, users.size]}
35
+ puts
36
+
37
+ # 4. now we want to clean up the actors that are covered by the group to slim down
38
+ # the actor set size. So we loop through actors and remove them if group returns
39
+ # true for the provided actor and context.
40
+ Flipper[:stats].actors_value.each do |flipper_id|
41
+ # Hydrate the flipper_id into an active record object. Modify this based on
42
+ # your flipper_id's if you use anything other than active record models and
43
+ # the default flipper_id provided by flipper.
44
+ class_name, id = flipper_id.split(';')
45
+ klass = class_name.constantize
46
+ user = klass.find(id)
47
+
48
+ # if user is in group then disable for actor because they'll still get the feature
49
+ context = Flipper::FeatureCheckContext.new(
50
+ feature_name: :stats,
51
+ values: Flipper[:stats].gate_values,
52
+ actors: [Flipper::Types::Actor.wrap(user)]
53
+ )
54
+
55
+ if Flipper::Gates::Group.new.open?(context)
56
+ Flipper.disable(:stats, user)
57
+ end
58
+ end
59
+
60
+ # check enabled again, should be the same result as previous checks
61
+ print 'Should be [[true, 100]]: '
62
+ print users.group_by { |user| Flipper.enabled?(:stats, user) }.map { |result, users| [result, users.size]}
63
+ puts
64
+
65
+ puts "Actors enabled: #{Flipper[:stats].actors_value.size}"
66
+ puts "Groups enabled: #{Flipper[:stats].groups_value.size}"
67
+
68
+ puts "All actors that could be migrated to groups were migrated. Yay!"
@@ -32,5 +32,5 @@ Gem::Specification.new do |gem|
32
32
  gem.metadata = Flipper::METADATA
33
33
 
34
34
  gem.add_dependency 'flipper', "~> #{Flipper::VERSION}"
35
- gem.add_dependency 'activerecord', '>= 4.2', '< 8'
35
+ gem.add_dependency 'activerecord', '>= 4.2', '< 9'
36
36
  end
@@ -1,4 +1,5 @@
1
1
  require 'set'
2
+ require 'securerandom'
2
3
  require 'flipper'
3
4
  require 'active_record'
4
5
 
@@ -7,7 +8,6 @@ module Flipper
7
8
  class ActiveRecord
8
9
  include ::Flipper::Adapter
9
10
 
10
- # Abstract base class for internal models
11
11
  class Model < ::ActiveRecord::Base
12
12
  self.abstract_class = true
13
13
  end
@@ -38,7 +38,7 @@ module Flipper
38
38
  end
39
39
 
40
40
  VALUE_TO_TEXT_WARNING = <<-EOS
41
- Your database needs migrated to use the latest Flipper features.
41
+ Your database needs to be migrated to use the latest Flipper features.
42
42
  Run `rails generate flipper:update` and `rails db:migrate`.
43
43
  EOS
44
44
 
@@ -56,10 +56,8 @@ module Flipper
56
56
  # can roll your own tables and what not, if you so desire.
57
57
  def initialize(options = {})
58
58
  @name = options.fetch(:name, :active_record)
59
- @feature_class = options.fetch(:feature_class) { Feature }
60
- @gate_class = options.fetch(:gate_class) { Gate }
61
-
62
- warn VALUE_TO_TEXT_WARNING if value_not_text?
59
+ @feature_class = options.fetch(:feature_class) { Flipper::Adapters::ActiveRecord::Feature }
60
+ @gate_class = options.fetch(:gate_class) { Flipper::Adapters::ActiveRecord::Gate }
63
61
  end
64
62
 
65
63
  # Public: The set of known features.
@@ -70,14 +68,15 @@ module Flipper
70
68
  # Public: Adds a feature to the set of known features.
71
69
  def add(feature)
72
70
  with_connection(@feature_class) do
73
- # race condition, but add is only used by enable/disable which happen
74
- # super rarely, so it shouldn't matter in practice
75
- @feature_class.transaction do
76
- unless @feature_class.where(key: feature.key).exists?
77
- begin
71
+ @feature_class.transaction(requires_new: true) do
72
+ begin
73
+ # race condition, but add is only used by enable/disable which happen
74
+ # super rarely, so it shouldn't matter in practice
75
+ unless @feature_class.where(key: feature.key).exists?
78
76
  @feature_class.create!(key: feature.key)
79
- rescue ::ActiveRecord::RecordNotUnique
80
77
  end
78
+ rescue ::ActiveRecord::RecordNotUnique
79
+ # already added
81
80
  end
82
81
  end
83
82
  end
@@ -128,14 +127,14 @@ module Flipper
128
127
  end
129
128
 
130
129
  def get_all
131
- with_connection(@feature_class) do
130
+ with_connection(@feature_class) do |connection|
132
131
  # query the gates from the db in a single query
133
132
  features = ::Arel::Table.new(@feature_class.table_name.to_sym)
134
133
  gates = ::Arel::Table.new(@gate_class.table_name.to_sym)
135
134
  rows_query = features.join(gates, ::Arel::Nodes::OuterJoin)
136
135
  .on(features[:key].eq(gates[:feature_key]))
137
136
  .project(features[:key].as('feature_key'), gates[:key], gates[:value])
138
- gates = @feature_class.connection.select_rows(rows_query)
137
+ gates = connection.select_rows(rows_query)
139
138
 
140
139
  # group the gates by feature key
141
140
  grouped_gates = gates.inject({}) do |hash, (feature_key, key, value)|
@@ -218,10 +217,9 @@ module Flipper
218
217
  raise VALUE_TO_TEXT_WARNING if json_feature && value_not_text?
219
218
 
220
219
  with_connection(@gate_class) do
221
- @gate_class.transaction do
220
+ @gate_class.transaction(requires_new: true) do
222
221
  clear(feature) if clear_feature
223
222
  delete(feature, gate)
224
- @gate_class.where(feature_key: feature.key, key: gate.key).destroy_all
225
223
  begin
226
224
  @gate_class.create! do |g|
227
225
  g.feature_key = feature.key
@@ -243,17 +241,21 @@ module Flipper
243
241
  end
244
242
 
245
243
  def enable_multi(feature, gate, thing)
246
- with_connection(@gate_class) do
247
- @gate_class.create! do |g|
248
- g.feature_key = feature.key
249
- g.key = gate.key
250
- g.value = thing.value.to_s
244
+ with_connection(@gate_class) do |connection|
245
+ begin
246
+ connection.transaction(requires_new: true) do
247
+ @gate_class.create! do |g|
248
+ g.feature_key = feature.key
249
+ g.key = gate.key
250
+ g.value = thing.value.to_s
251
+ end
252
+ end
253
+ rescue ::ActiveRecord::RecordNotUnique
254
+ # already added so move on with life
251
255
  end
252
256
  end
253
257
 
254
258
  nil
255
- rescue ::ActiveRecord::RecordNotUnique
256
- # already added so no need move on with life
257
259
  end
258
260
 
259
261
  def result_for_gates(feature, gates)
@@ -282,15 +284,23 @@ module Flipper
282
284
  # Check if value column is text instead of string
283
285
  # See https://github.com/flippercloud/flipper/pull/692
284
286
  def value_not_text?
285
- @gate_class.column_for_attribute(:value).type != :text
287
+ with_connection(@gate_class) do |connection|
288
+ @gate_class.column_for_attribute(:value).type != :text
289
+ end
286
290
  rescue ::ActiveRecord::ActiveRecordError => error
287
291
  # If the table doesn't exist, the column doesn't exist either
288
292
  warn "#{error.message}. You likely need to run `rails g flipper:active_record` and/or `rails db:migrate`."
289
293
  end
290
294
 
291
295
  def with_connection(model = @feature_class, &block)
296
+ warn VALUE_TO_TEXT_WARNING if !warned_about_value_not_text? && value_not_text?
292
297
  model.connection_pool.with_connection(&block)
293
298
  end
299
+
300
+ def warned_about_value_not_text?
301
+ return @warned_about_value_not_text if defined?(@warned_about_value_not_text)
302
+ @warned_about_value_not_text = true
303
+ end
294
304
  end
295
305
  end
296
306
  end
@@ -1,5 +1,5 @@
1
1
  module Flipper
2
- VERSION = '1.3.0'.freeze
2
+ VERSION = '1.3.5'.freeze
3
3
 
4
4
  REQUIRED_RUBY_VERSION = '2.6'.freeze
5
5
  NEXT_REQUIRED_RUBY_VERSION = '3.0'.freeze
@@ -9,9 +9,10 @@ RSpec.describe Flipper::Adapters::ActiveRecord do
9
9
 
10
10
  before(:all) do
11
11
  # Eval migration template so we can run migration against each database
12
- migration = ERB.new(File.read(File.join(File.dirname(__FILE__), '../../../lib/generators/flipper/templates/migration.erb')))
12
+ template_path = File.join(File.dirname(__FILE__), '../../../lib/generators/flipper/templates/migration.erb')
13
+ migration = ERB.new(File.read(template_path))
13
14
  migration_version = "[#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}]"
14
- eval migration.result(binding) # defines CreateFlipperTables
15
+ eval migration.result_with_hash(migration_version: migration_version) # defines CreateFlipperTables
15
16
  end
16
17
 
17
18
  [
@@ -23,6 +24,7 @@ RSpec.describe Flipper::Adapters::ActiveRecord do
23
24
  {
24
25
  "adapter" => "mysql2",
25
26
  "encoding" => "utf8mb4",
27
+ "host" => ENV["MYSQL_HOST"],
26
28
  "username" => ENV["MYSQL_USER"] || "root",
27
29
  "password" => ENV["MYSQL_PASSWORD"] || "",
28
30
  "database" => ENV["MYSQL_DATABASE"] || "flipper_test",
@@ -42,24 +44,21 @@ RSpec.describe Flipper::Adapters::ActiveRecord do
42
44
  context "with tables created" do
43
45
  before(:all) do
44
46
  skip_on_error(ActiveRecord::ConnectionNotEstablished, "#{config['adapter']} not available") do
45
- silence { ActiveRecord::Tasks::DatabaseTasks.create(config) }
47
+ silence do
48
+ ActiveRecord::Tasks::DatabaseTasks.create(config)
49
+ end
46
50
  end
47
51
 
48
52
  Flipper.configuration = nil
49
53
  end
50
54
 
51
55
  before(:each) do
52
- skip_on_error(ActiveRecord::ConnectionNotEstablished, "#{config['adapter']} not available") do
53
- ActiveRecord::Base.establish_connection(config)
56
+ silence do
57
+ ActiveRecord::Tasks::DatabaseTasks.purge(config)
54
58
  CreateFlipperTables.migrate(:up)
55
59
  end
56
60
  end
57
61
 
58
- after(:each) do
59
- ActiveRecord::Tasks::DatabaseTasks.purge(config)
60
- ActiveRecord::Base.connection.close
61
- end
62
-
63
62
  after(:all) do
64
63
  silence { ActiveRecord::Tasks::DatabaseTasks.drop(config) } unless $skip
65
64
  end
@@ -79,6 +78,20 @@ RSpec.describe Flipper::Adapters::ActiveRecord do
79
78
  flipper.preload([:foo])
80
79
  end
81
80
 
81
+ it 'should not poison wrapping transactions' do
82
+ flipper = Flipper.new(subject)
83
+
84
+ actor = Struct.new(:flipper_id).new('flipper-id-123')
85
+ flipper.enable_actor(:foo, actor)
86
+
87
+ ActiveRecord::Base.transaction do
88
+ flipper.enable_actor(:foo, actor)
89
+ # any read on the next line is fine, just need to ensure that
90
+ # poisoned transaction isn't raised
91
+ expect(Flipper::Adapters::ActiveRecord::Gate.count).to eq(1)
92
+ end
93
+ end
94
+
82
95
  context "ActiveRecord connection_pool" do
83
96
  before do
84
97
  ActiveRecord::Base.connection_handler.clear_active_connections!
@@ -215,18 +228,39 @@ RSpec.describe Flipper::Adapters::ActiveRecord do
215
228
  end
216
229
  end
217
230
 
218
- it "works when table doesn't exist" do
231
+ context "without tables created" do
232
+ before(:all) do
233
+ skip_on_error(ActiveRecord::ConnectionNotEstablished, "#{config['adapter']} not available") do
234
+ silence do
235
+ ActiveRecord::Tasks::DatabaseTasks.create(config)
236
+ end
237
+ end
219
238
 
220
- Flipper.configuration = nil
221
- Flipper.instance = nil
239
+ Flipper.configuration = nil
240
+ end
241
+
242
+ before(:each) do
243
+ ActiveRecord::Base.establish_connection(config)
244
+ end
222
245
 
223
- Flipper::Adapters.send(:remove_const, :ActiveRecord) if Flipper::Adapters.const_defined?(:ActiveRecord)
246
+ after(:each) do
247
+ ActiveRecord::Base.connection.close
248
+ end
224
249
 
225
- silence do
226
- expect {
227
- load 'flipper/adapters/active_record.rb'
228
- Flipper::Adapters::ActiveRecord.new
229
- }.not_to raise_error
250
+ after(:all) do
251
+ silence { ActiveRecord::Tasks::DatabaseTasks.drop(config) } unless $skip
252
+ end
253
+
254
+ it "does not raise an error" do
255
+ Flipper.configuration = nil
256
+ Flipper.instance = nil
257
+
258
+ silence do
259
+ expect {
260
+ load 'flipper/adapters/active_record.rb'
261
+ Flipper::Adapters::ActiveRecord.new
262
+ }.not_to raise_error
263
+ end
230
264
  end
231
265
  end
232
266
  end
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: flipper-active_record
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.3.0
4
+ version: 1.3.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - John Nunemaker
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2024-04-17 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: flipper
@@ -16,14 +15,14 @@ dependencies:
16
15
  requirements:
17
16
  - - "~>"
18
17
  - !ruby/object:Gem::Version
19
- version: 1.3.0
18
+ version: 1.3.5
20
19
  type: :runtime
21
20
  prerelease: false
22
21
  version_requirements: !ruby/object:Gem::Requirement
23
22
  requirements:
24
23
  - - "~>"
25
24
  - !ruby/object:Gem::Version
26
- version: 1.3.0
25
+ version: 1.3.5
27
26
  - !ruby/object:Gem::Dependency
28
27
  name: activerecord
29
28
  requirement: !ruby/object:Gem::Requirement
@@ -33,7 +32,7 @@ dependencies:
33
32
  version: '4.2'
34
33
  - - "<"
35
34
  - !ruby/object:Gem::Version
36
- version: '8'
35
+ version: '9'
37
36
  type: :runtime
38
37
  prerelease: false
39
38
  version_requirements: !ruby/object:Gem::Requirement
@@ -43,8 +42,7 @@ dependencies:
43
42
  version: '4.2'
44
43
  - - "<"
45
44
  - !ruby/object:Gem::Version
46
- version: '8'
47
- description:
45
+ version: '9'
48
46
  email: support@flippercloud.io
49
47
  executables: []
50
48
  extensions: []
@@ -56,6 +54,7 @@ files:
56
54
  - examples/active_record/ar_setup.rb
57
55
  - examples/active_record/basic.rb
58
56
  - examples/active_record/cached.rb
57
+ - examples/active_record/group_migration.rb
59
58
  - examples/active_record/internals.rb
60
59
  - flipper-active_record.gemspec
61
60
  - lib/flipper-active_record.rb
@@ -74,8 +73,8 @@ metadata:
74
73
  homepage_uri: https://www.flippercloud.io
75
74
  source_code_uri: https://github.com/flippercloud/flipper
76
75
  bug_tracker_uri: https://github.com/flippercloud/flipper/issues
77
- changelog_uri: https://github.com/flippercloud/flipper/releases/tag/v1.3.0
78
- post_install_message:
76
+ changelog_uri: https://github.com/flippercloud/flipper/releases/tag/v1.3.5
77
+ funding_uri: https://github.com/sponsors/flippercloud
79
78
  rdoc_options: []
80
79
  require_paths:
81
80
  - lib
@@ -90,8 +89,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
90
89
  - !ruby/object:Gem::Version
91
90
  version: '0'
92
91
  requirements: []
93
- rubygems_version: 3.5.3
94
- signing_key:
92
+ rubygems_version: 3.6.9
95
93
  specification_version: 4
96
94
  summary: ActiveRecord feature flag adapter for Flipper
97
95
  test_files: