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 +4 -4
- data/examples/active_record/ar_setup.rb +15 -0
- data/examples/active_record/group_migration.rb +68 -0
- data/flipper-active_record.gemspec +1 -1
- data/lib/flipper/adapters/active_record.rb +34 -24
- data/lib/flipper/version.rb +1 -1
- data/spec/flipper/adapters/active_record_spec.rb +53 -19
- metadata +10 -12
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 6db1458b33ebce656ac2fd6a0aadeb5e2547b5cd346644ec64cf54034f93a461
|
4
|
+
data.tar.gz: ab9c8dee420bef036736f998a99411a89c70cdc794cdb96ec0af355a5a682902
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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!"
|
@@ -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
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
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 =
|
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
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
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
|
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
|
data/lib/flipper/version.rb
CHANGED
@@ -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
|
-
|
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.
|
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
|
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
|
-
|
53
|
-
ActiveRecord::
|
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
|
-
|
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
|
-
|
221
|
-
|
239
|
+
Flipper.configuration = nil
|
240
|
+
end
|
241
|
+
|
242
|
+
before(:each) do
|
243
|
+
ActiveRecord::Base.establish_connection(config)
|
244
|
+
end
|
222
245
|
|
223
|
-
|
246
|
+
after(:each) do
|
247
|
+
ActiveRecord::Base.connection.close
|
248
|
+
end
|
224
249
|
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
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.
|
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:
|
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.
|
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.
|
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: '
|
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: '
|
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.
|
78
|
-
|
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.
|
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:
|