flipper-active_record 1.0.0 → 1.4.0
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/benchmark/active_record_adapter_ips.rb +5 -2
- data/examples/active_record/ar_setup.rb +15 -0
- data/examples/active_record/group_migration.rb +68 -0
- data/flipper-active_record.gemspec +10 -3
- data/lib/flipper/adapters/active_record/feature.rb +20 -0
- data/lib/flipper/adapters/active_record/gate.rb +19 -0
- data/lib/flipper/adapters/active_record/model.rb +9 -0
- data/lib/flipper/adapters/active_record.rb +96 -56
- data/lib/flipper/version.rb +11 -1
- data/lib/generators/flipper/templates/migration.erb +4 -4
- data/spec/flipper/adapters/active_record_spec.rb +345 -146
- data/test/adapters/active_record_test.rb +20 -6
- data/test_rails/generators/flipper/active_record_generator_test.rb +4 -6
- metadata +15 -12
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d550530c982b783d5bb01547b0dd63e65ae28cf684baf5cab91f633b45546a6a
|
|
4
|
+
data.tar.gz: abe3da9138ef86cc0a7fecf5496d4ac78b036cfb9f8188b782debf9be8ab92ba
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: fe247ffa161d7090ff3630a50329f128c75b7ef65109b9a9fb85bd4432892752d7f106b21bf05f9cf5de913ed43e72f18d9316e9775300d9cc7be4c571c8f492
|
|
7
|
+
data.tar.gz: 82583bfa629d02c3eeba04b9cb7b32ba78ff65fcb39bba598c07e9a9dc843a486fe3a08351b528db3a1d032a7632593af92a6b1045c0d2af568c30974474c684
|
|
@@ -6,10 +6,13 @@ require 'benchmark/ips'
|
|
|
6
6
|
|
|
7
7
|
flipper = Flipper.new(Flipper::Adapters::ActiveRecord.new)
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
10.times do |n|
|
|
10
|
+
2000.times do |i|
|
|
11
|
+
flipper.enable_actor 'feature' + n.to_s, Flipper::Actor.new("User;#{i}")
|
|
12
|
+
end
|
|
11
13
|
end
|
|
12
14
|
|
|
13
15
|
Benchmark.ips do |x|
|
|
14
16
|
x.report("get_all") { flipper.preload_all }
|
|
17
|
+
x.report("features") { flipper.features }
|
|
15
18
|
end
|
|
@@ -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,15 +1,22 @@
|
|
|
1
1
|
# -*- encoding: utf-8 -*-
|
|
2
2
|
require File.expand_path('../lib/flipper/version', __FILE__)
|
|
3
3
|
require File.expand_path('../lib/flipper/metadata', __FILE__)
|
|
4
|
+
require "set"
|
|
5
|
+
|
|
6
|
+
# Files that should exist in main flipper gem.
|
|
7
|
+
main_flipper_active_record_files = Set[
|
|
8
|
+
"lib/flipper/model/active_record.rb",
|
|
9
|
+
"spec/flipper/model/active_record_spec.rb",
|
|
10
|
+
]
|
|
4
11
|
|
|
5
12
|
flipper_active_record_files = lambda do |file|
|
|
6
|
-
file =~ /active_record/
|
|
13
|
+
file =~ /active_record/ && !main_flipper_active_record_files.include?(file)
|
|
7
14
|
end
|
|
8
15
|
|
|
9
16
|
Gem::Specification.new do |gem|
|
|
10
17
|
gem.authors = ['John Nunemaker']
|
|
11
18
|
gem.email = 'support@flippercloud.io'
|
|
12
|
-
gem.summary = 'ActiveRecord adapter for Flipper'
|
|
19
|
+
gem.summary = 'ActiveRecord feature flag adapter for Flipper'
|
|
13
20
|
gem.license = 'MIT'
|
|
14
21
|
gem.homepage = 'https://www.flippercloud.io/docs/adapters/active-record'
|
|
15
22
|
|
|
@@ -25,5 +32,5 @@ Gem::Specification.new do |gem|
|
|
|
25
32
|
gem.metadata = Flipper::METADATA
|
|
26
33
|
|
|
27
34
|
gem.add_dependency 'flipper', "~> #{Flipper::VERSION}"
|
|
28
|
-
gem.add_dependency 'activerecord', '>= 4.2', '<
|
|
35
|
+
gem.add_dependency 'activerecord', '>= 4.2', '< 9'
|
|
29
36
|
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
require 'flipper/adapters/active_record/model'
|
|
2
|
+
|
|
3
|
+
module Flipper
|
|
4
|
+
module Adapters
|
|
5
|
+
class ActiveRecord
|
|
6
|
+
# Private: Do not use outside of this adapter.
|
|
7
|
+
class Feature < Model
|
|
8
|
+
self.table_name = [
|
|
9
|
+
Model.table_name_prefix,
|
|
10
|
+
"flipper_features",
|
|
11
|
+
Model.table_name_suffix,
|
|
12
|
+
].join
|
|
13
|
+
|
|
14
|
+
has_many :gates, foreign_key: "feature_key", primary_key: "key"
|
|
15
|
+
|
|
16
|
+
validates :key, presence: true
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
require 'flipper/adapters/active_record/model'
|
|
2
|
+
|
|
3
|
+
module Flipper
|
|
4
|
+
module Adapters
|
|
5
|
+
class ActiveRecord
|
|
6
|
+
# Private: Do not use outside of this adapter.
|
|
7
|
+
class Gate < Model
|
|
8
|
+
self.table_name = [
|
|
9
|
+
Model.table_name_prefix,
|
|
10
|
+
"flipper_gates",
|
|
11
|
+
Model.table_name_suffix,
|
|
12
|
+
].join
|
|
13
|
+
|
|
14
|
+
validates :feature_key, presence: true
|
|
15
|
+
validates :key, presence: true
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -1,37 +1,20 @@
|
|
|
1
1
|
require 'set'
|
|
2
|
+
require 'securerandom'
|
|
2
3
|
require 'flipper'
|
|
3
4
|
require 'active_record'
|
|
5
|
+
require_relative 'active_record/model'
|
|
6
|
+
require_relative 'active_record/feature'
|
|
7
|
+
require_relative 'active_record/gate'
|
|
4
8
|
|
|
5
9
|
module Flipper
|
|
6
10
|
module Adapters
|
|
7
11
|
class ActiveRecord
|
|
8
12
|
include ::Flipper::Adapter
|
|
9
13
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
# Private: Do not use outside of this adapter.
|
|
16
|
-
class Feature < Model
|
|
17
|
-
self.table_name = [
|
|
18
|
-
Model.table_name_prefix,
|
|
19
|
-
"flipper_features",
|
|
20
|
-
Model.table_name_suffix,
|
|
21
|
-
].join
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
# Private: Do not use outside of this adapter.
|
|
25
|
-
class Gate < Model
|
|
26
|
-
self.table_name = [
|
|
27
|
-
Model.table_name_prefix,
|
|
28
|
-
"flipper_gates",
|
|
29
|
-
Model.table_name_suffix,
|
|
30
|
-
].join
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
# Public: The name of the adapter.
|
|
34
|
-
attr_reader :name
|
|
14
|
+
VALUE_TO_TEXT_WARNING = <<-EOS
|
|
15
|
+
Your database needs to be migrated to use the latest Flipper features.
|
|
16
|
+
Run `rails generate flipper:update` and `rails db:migrate`.
|
|
17
|
+
EOS
|
|
35
18
|
|
|
36
19
|
# Public: Initialize a new ActiveRecord adapter instance.
|
|
37
20
|
#
|
|
@@ -47,26 +30,27 @@ module Flipper
|
|
|
47
30
|
# can roll your own tables and what not, if you so desire.
|
|
48
31
|
def initialize(options = {})
|
|
49
32
|
@name = options.fetch(:name, :active_record)
|
|
50
|
-
@feature_class = options.fetch(:feature_class) { Feature }
|
|
51
|
-
@gate_class = options.fetch(:gate_class) { Gate }
|
|
33
|
+
@feature_class = options.fetch(:feature_class) { Flipper::Adapters::ActiveRecord::Feature }
|
|
34
|
+
@gate_class = options.fetch(:gate_class) { Flipper::Adapters::ActiveRecord::Gate }
|
|
52
35
|
end
|
|
53
36
|
|
|
54
37
|
# Public: The set of known features.
|
|
55
38
|
def features
|
|
56
|
-
with_connection(@feature_class) { @feature_class.
|
|
39
|
+
with_connection(@feature_class) { @feature_class.distinct.pluck(:key).to_set }
|
|
57
40
|
end
|
|
58
41
|
|
|
59
42
|
# Public: Adds a feature to the set of known features.
|
|
60
43
|
def add(feature)
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
@feature_class.create!
|
|
68
|
-
rescue ::ActiveRecord::RecordNotUnique
|
|
44
|
+
with_write_connection(@feature_class) do
|
|
45
|
+
@feature_class.transaction(requires_new: true) do
|
|
46
|
+
begin
|
|
47
|
+
# race condition, but add is only used by enable/disable which happen
|
|
48
|
+
# super rarely, so it shouldn't matter in practice
|
|
49
|
+
unless @feature_class.where(key: feature.key).exists?
|
|
50
|
+
@feature_class.create!(key: feature.key)
|
|
69
51
|
end
|
|
52
|
+
rescue ::ActiveRecord::RecordNotUnique
|
|
53
|
+
# already added
|
|
70
54
|
end
|
|
71
55
|
end
|
|
72
56
|
end
|
|
@@ -76,7 +60,7 @@ module Flipper
|
|
|
76
60
|
|
|
77
61
|
# Public: Removes a feature from the set of known features.
|
|
78
62
|
def remove(feature)
|
|
79
|
-
|
|
63
|
+
with_write_connection(@feature_class) do
|
|
80
64
|
@feature_class.transaction do
|
|
81
65
|
@feature_class.where(key: feature.key).destroy_all
|
|
82
66
|
clear(feature)
|
|
@@ -87,7 +71,7 @@ module Flipper
|
|
|
87
71
|
|
|
88
72
|
# Public: Clears the gate values for a feature.
|
|
89
73
|
def clear(feature)
|
|
90
|
-
|
|
74
|
+
with_write_connection(@gate_class) { @gate_class.where(feature_key: feature.key).destroy_all }
|
|
91
75
|
true
|
|
92
76
|
end
|
|
93
77
|
|
|
@@ -116,15 +100,15 @@ module Flipper
|
|
|
116
100
|
end
|
|
117
101
|
end
|
|
118
102
|
|
|
119
|
-
def get_all
|
|
120
|
-
with_connection(@feature_class) do
|
|
103
|
+
def get_all(**kwargs)
|
|
104
|
+
with_connection(@feature_class) do |connection|
|
|
121
105
|
# query the gates from the db in a single query
|
|
122
106
|
features = ::Arel::Table.new(@feature_class.table_name.to_sym)
|
|
123
107
|
gates = ::Arel::Table.new(@gate_class.table_name.to_sym)
|
|
124
108
|
rows_query = features.join(gates, ::Arel::Nodes::OuterJoin)
|
|
125
109
|
.on(features[:key].eq(gates[:feature_key]))
|
|
126
110
|
.project(features[:key].as('feature_key'), gates[:key], gates[:value])
|
|
127
|
-
gates =
|
|
111
|
+
gates = connection.select_rows(rows_query)
|
|
128
112
|
|
|
129
113
|
# group the gates by feature key
|
|
130
114
|
grouped_gates = gates.inject({}) do |hash, (feature_key, key, value)|
|
|
@@ -139,6 +123,7 @@ module Flipper
|
|
|
139
123
|
features.each do |feature|
|
|
140
124
|
result[feature.key] = result_for_gates(feature, grouped_gates[feature.key])
|
|
141
125
|
end
|
|
126
|
+
result.default_proc = nil
|
|
142
127
|
result
|
|
143
128
|
end
|
|
144
129
|
end
|
|
@@ -156,6 +141,8 @@ module Flipper
|
|
|
156
141
|
set(feature, gate, thing, clear: true)
|
|
157
142
|
when :integer
|
|
158
143
|
set(feature, gate, thing)
|
|
144
|
+
when :json
|
|
145
|
+
set(feature, gate, thing, json: true)
|
|
159
146
|
when :set
|
|
160
147
|
enable_multi(feature, gate, thing)
|
|
161
148
|
else
|
|
@@ -178,8 +165,12 @@ module Flipper
|
|
|
178
165
|
clear(feature)
|
|
179
166
|
when :integer
|
|
180
167
|
set(feature, gate, thing)
|
|
168
|
+
when :json
|
|
169
|
+
with_write_connection(@gate_class) do
|
|
170
|
+
delete(feature, gate)
|
|
171
|
+
end
|
|
181
172
|
when :set
|
|
182
|
-
|
|
173
|
+
with_write_connection(@gate_class) do
|
|
183
174
|
@gate_class.where(feature_key: feature.key, key: gate.key, value: thing.value).destroy_all
|
|
184
175
|
end
|
|
185
176
|
else
|
|
@@ -198,15 +189,19 @@ module Flipper
|
|
|
198
189
|
|
|
199
190
|
def set(feature, gate, thing, options = {})
|
|
200
191
|
clear_feature = options.fetch(:clear, false)
|
|
201
|
-
|
|
202
|
-
|
|
192
|
+
json_feature = options.fetch(:json, false)
|
|
193
|
+
|
|
194
|
+
raise VALUE_TO_TEXT_WARNING if json_feature && value_not_text?
|
|
195
|
+
|
|
196
|
+
with_write_connection(@gate_class) do
|
|
197
|
+
@gate_class.transaction(requires_new: true) do
|
|
203
198
|
clear(feature) if clear_feature
|
|
204
|
-
|
|
199
|
+
delete(feature, gate)
|
|
205
200
|
begin
|
|
206
201
|
@gate_class.create! do |g|
|
|
207
202
|
g.feature_key = feature.key
|
|
208
203
|
g.key = gate.key
|
|
209
|
-
g.value = thing.value.to_s
|
|
204
|
+
g.value = json_feature ? Typecast.to_json(thing.value) : thing.value.to_s
|
|
210
205
|
end
|
|
211
206
|
rescue ::ActiveRecord::RecordNotUnique
|
|
212
207
|
# assume this happened concurrently with the same thing and its fine
|
|
@@ -218,18 +213,26 @@ module Flipper
|
|
|
218
213
|
nil
|
|
219
214
|
end
|
|
220
215
|
|
|
216
|
+
def delete(feature, gate)
|
|
217
|
+
@gate_class.where(feature_key: feature.key, key: gate.key).destroy_all
|
|
218
|
+
end
|
|
219
|
+
|
|
221
220
|
def enable_multi(feature, gate, thing)
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
221
|
+
with_write_connection(@gate_class) do |connection|
|
|
222
|
+
begin
|
|
223
|
+
connection.transaction(requires_new: true) do
|
|
224
|
+
@gate_class.create! do |g|
|
|
225
|
+
g.feature_key = feature.key
|
|
226
|
+
g.key = gate.key
|
|
227
|
+
g.value = thing.value.to_s
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
rescue ::ActiveRecord::RecordNotUnique
|
|
231
|
+
# already added so move on with life
|
|
227
232
|
end
|
|
228
233
|
end
|
|
229
234
|
|
|
230
235
|
nil
|
|
231
|
-
rescue ::ActiveRecord::RecordNotUnique
|
|
232
|
-
# already added so no need move on with life
|
|
233
236
|
end
|
|
234
237
|
|
|
235
238
|
def result_for_gates(feature, gates)
|
|
@@ -238,13 +241,13 @@ module Flipper
|
|
|
238
241
|
feature.gates.each do |gate|
|
|
239
242
|
result[gate.key] =
|
|
240
243
|
case gate.data_type
|
|
241
|
-
when :boolean
|
|
244
|
+
when :boolean, :integer
|
|
242
245
|
if row = gates.detect { |key, value| !key.nil? && key.to_sym == gate.key }
|
|
243
246
|
row.last
|
|
244
247
|
end
|
|
245
|
-
when :
|
|
248
|
+
when :json
|
|
246
249
|
if row = gates.detect { |key, value| !key.nil? && key.to_sym == gate.key }
|
|
247
|
-
row.last
|
|
250
|
+
Typecast.from_json(row.last)
|
|
248
251
|
end
|
|
249
252
|
when :set
|
|
250
253
|
gates.select { |key, value| !key.nil? && key.to_sym == gate.key }.map(&:last).to_set
|
|
@@ -255,9 +258,46 @@ module Flipper
|
|
|
255
258
|
result
|
|
256
259
|
end
|
|
257
260
|
|
|
261
|
+
# Check if value column is text instead of string
|
|
262
|
+
# See https://github.com/flippercloud/flipper/pull/692
|
|
263
|
+
def value_not_text?
|
|
264
|
+
with_connection(@gate_class) do |connection|
|
|
265
|
+
@gate_class.column_for_attribute(:value).type != :text
|
|
266
|
+
end
|
|
267
|
+
rescue ::ActiveRecord::ActiveRecordError => error
|
|
268
|
+
# If the table doesn't exist, the column doesn't exist either
|
|
269
|
+
warn "#{error.message}. You likely need to run `rails g flipper:active_record` and/or `rails db:migrate`."
|
|
270
|
+
end
|
|
271
|
+
|
|
258
272
|
def with_connection(model = @feature_class, &block)
|
|
273
|
+
warn VALUE_TO_TEXT_WARNING if !warned_about_value_not_text? && value_not_text?
|
|
259
274
|
model.connection_pool.with_connection(&block)
|
|
260
275
|
end
|
|
276
|
+
|
|
277
|
+
def with_write_connection(model = @feature_class, &block)
|
|
278
|
+
# Use Rails' built-in method to find the class that controls the connection
|
|
279
|
+
# This walks up the inheritance chain to find which class called connects_to
|
|
280
|
+
if model.respond_to?(:connection_class_for_self)
|
|
281
|
+
connection_class = model.connection_class_for_self
|
|
282
|
+
|
|
283
|
+
# Only use connected_to if this class actually has connects_to configured
|
|
284
|
+
# connection_class? returns true when connects_to was called on the class
|
|
285
|
+
if connection_class.respond_to?(:connection_class?) && connection_class.connection_class?
|
|
286
|
+
connection_class.connected_to(role: :writing) do
|
|
287
|
+
with_connection(model, &block)
|
|
288
|
+
end
|
|
289
|
+
else
|
|
290
|
+
with_connection(model, &block)
|
|
291
|
+
end
|
|
292
|
+
else
|
|
293
|
+
with_connection(model, &block)
|
|
294
|
+
end
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
def warned_about_value_not_text?
|
|
298
|
+
return @warned_about_value_not_text if defined?(@warned_about_value_not_text)
|
|
299
|
+
@warned_about_value_not_text = true
|
|
300
|
+
end
|
|
261
301
|
end
|
|
262
302
|
end
|
|
263
303
|
end
|
data/lib/flipper/version.rb
CHANGED
|
@@ -1,3 +1,13 @@
|
|
|
1
1
|
module Flipper
|
|
2
|
-
VERSION = '1.
|
|
2
|
+
VERSION = '1.4.0'.freeze
|
|
3
|
+
|
|
4
|
+
REQUIRED_RUBY_VERSION = '2.6'.freeze
|
|
5
|
+
NEXT_REQUIRED_RUBY_VERSION = '3.0'.freeze
|
|
6
|
+
|
|
7
|
+
REQUIRED_RAILS_VERSION = '5.2'.freeze
|
|
8
|
+
NEXT_REQUIRED_RAILS_VERSION = '6.1.0'.freeze
|
|
9
|
+
|
|
10
|
+
def self.deprecated_ruby_version?
|
|
11
|
+
Gem::Version.new(RUBY_VERSION) < Gem::Version.new(NEXT_REQUIRED_RUBY_VERSION)
|
|
12
|
+
end
|
|
3
13
|
end
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
class CreateFlipperTables < ActiveRecord::Migration<%= migration_version %>
|
|
2
|
-
def
|
|
2
|
+
def up
|
|
3
3
|
create_table :flipper_features do |t|
|
|
4
4
|
t.string :key, null: false
|
|
5
5
|
t.timestamps null: false
|
|
@@ -9,13 +9,13 @@ class CreateFlipperTables < ActiveRecord::Migration<%= migration_version %>
|
|
|
9
9
|
create_table :flipper_gates do |t|
|
|
10
10
|
t.string :feature_key, null: false
|
|
11
11
|
t.string :key, null: false
|
|
12
|
-
t.
|
|
12
|
+
t.text :value
|
|
13
13
|
t.timestamps null: false
|
|
14
14
|
end
|
|
15
|
-
add_index :flipper_gates, [:feature_key, :key, :value], unique: true
|
|
15
|
+
add_index :flipper_gates, [:feature_key, :key, :value], unique: true, length: { value: 255 }
|
|
16
16
|
end
|
|
17
17
|
|
|
18
|
-
def
|
|
18
|
+
def down
|
|
19
19
|
drop_table :flipper_gates
|
|
20
20
|
drop_table :flipper_features
|
|
21
21
|
end
|
|
@@ -1,182 +1,381 @@
|
|
|
1
|
-
require 'flipper/adapters/active_record'
|
|
1
|
+
SpecHelpers.silence { require 'flipper/adapters/active_record' }
|
|
2
2
|
|
|
3
3
|
# Turn off migration logging for specs
|
|
4
4
|
ActiveRecord::Migration.verbose = false
|
|
5
|
+
ActiveRecord::Tasks::DatabaseTasks.root = File.dirname(__FILE__)
|
|
5
6
|
|
|
6
7
|
RSpec.describe Flipper::Adapters::ActiveRecord do
|
|
7
8
|
subject { described_class.new }
|
|
8
9
|
|
|
9
10
|
before(:all) do
|
|
10
|
-
|
|
11
|
-
|
|
11
|
+
# Eval migration template so we can run migration against each database
|
|
12
|
+
template_path = File.join(File.dirname(__FILE__), '../../../lib/generators/flipper/templates/migration.erb')
|
|
13
|
+
migration = ERB.new(File.read(template_path))
|
|
14
|
+
migration_version = "[#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}]"
|
|
15
|
+
eval migration.result_with_hash(migration_version: migration_version) # defines CreateFlipperTables
|
|
12
16
|
end
|
|
13
17
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
created_at datetime NOT NULL,
|
|
20
|
-
updated_at datetime NOT NULL
|
|
21
|
-
)
|
|
22
|
-
SQL
|
|
23
|
-
|
|
24
|
-
ActiveRecord::Base.connection.execute <<-SQL
|
|
25
|
-
CREATE TABLE flipper_gates (
|
|
26
|
-
id integer PRIMARY KEY,
|
|
27
|
-
feature_key text NOT NULL,
|
|
28
|
-
key text NOT NULL,
|
|
29
|
-
value text DEFAULT NULL,
|
|
30
|
-
created_at datetime NOT NULL,
|
|
31
|
-
updated_at datetime NOT NULL
|
|
32
|
-
)
|
|
33
|
-
SQL
|
|
34
|
-
|
|
35
|
-
ActiveRecord::Base.connection.execute <<-SQL
|
|
36
|
-
CREATE UNIQUE INDEX index_gates_on_keys_and_value on flipper_gates (feature_key, key, value)
|
|
37
|
-
SQL
|
|
38
|
-
end
|
|
18
|
+
[
|
|
19
|
+
{
|
|
20
|
+
"adapter" => "sqlite3",
|
|
21
|
+
"database" => ":memory:"
|
|
22
|
+
},
|
|
39
23
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
24
|
+
{
|
|
25
|
+
"adapter" => "mysql2",
|
|
26
|
+
"encoding" => "utf8mb4",
|
|
27
|
+
"host" => ENV["MYSQL_HOST"],
|
|
28
|
+
"username" => ENV["MYSQL_USER"] || "root",
|
|
29
|
+
"password" => ENV["MYSQL_PASSWORD"] || "",
|
|
30
|
+
"database" => ENV["MYSQL_DATABASE"] || "flipper_test",
|
|
31
|
+
"port" => ENV["DB_PORT"] || 3306
|
|
32
|
+
},
|
|
44
33
|
|
|
45
|
-
|
|
34
|
+
{
|
|
35
|
+
"adapter" => "postgresql",
|
|
36
|
+
"encoding" => "unicode",
|
|
37
|
+
"host" => "127.0.0.1",
|
|
38
|
+
"username" => ENV["POSTGRES_USER"] || "",
|
|
39
|
+
"password" => ENV["POSTGRES_PASSWORD"] || "",
|
|
40
|
+
"database" => ENV["POSTGRES_DATABASE"] || "flipper_test",
|
|
41
|
+
}
|
|
42
|
+
].each do |config|
|
|
43
|
+
context "with #{config['adapter']}" do
|
|
44
|
+
context "with tables created" do
|
|
45
|
+
before(:all) do
|
|
46
|
+
skip_on_error(ActiveRecord::ConnectionNotEstablished, "#{config['adapter']} not available") do
|
|
47
|
+
silence do
|
|
48
|
+
ActiveRecord::Tasks::DatabaseTasks.create(config)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
46
51
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
Flipper.configuration = nil
|
|
50
|
-
Flipper.instance = nil
|
|
52
|
+
Flipper.configuration = nil
|
|
53
|
+
end
|
|
51
54
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
+
before(:each) do
|
|
56
|
+
silence do
|
|
57
|
+
ActiveRecord::Tasks::DatabaseTasks.purge(config)
|
|
58
|
+
CreateFlipperTables.migrate(:up)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
55
61
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
end
|
|
62
|
+
after(:all) do
|
|
63
|
+
silence { ActiveRecord::Tasks::DatabaseTasks.drop(config) } unless $skip
|
|
64
|
+
end
|
|
60
65
|
|
|
61
|
-
|
|
62
|
-
before do
|
|
63
|
-
ActiveRecord::Base.clear_active_connections!
|
|
64
|
-
end
|
|
66
|
+
it_should_behave_like 'a flipper adapter'
|
|
65
67
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
expect(ActiveRecord::Base.connection_handler.active_connections?).to be(false)
|
|
69
|
-
subject.features
|
|
70
|
-
expect(ActiveRecord::Base.connection_handler.active_connections?).to be(false)
|
|
71
|
-
end
|
|
68
|
+
it "should load actor ids fine" do
|
|
69
|
+
flipper.enable_percentage_of_time(:foo, 1)
|
|
72
70
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
end
|
|
79
|
-
end
|
|
71
|
+
Flipper::Adapters::ActiveRecord::Gate.create!(
|
|
72
|
+
feature_key: "foo",
|
|
73
|
+
key: "actors",
|
|
74
|
+
value: "Organization;4",
|
|
75
|
+
)
|
|
80
76
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
subject.get_all
|
|
85
|
-
expect(ActiveRecord::Base.connection_handler.active_connections?).to be(false)
|
|
86
|
-
end
|
|
77
|
+
flipper = Flipper.new(subject)
|
|
78
|
+
flipper.preload([:foo])
|
|
79
|
+
end
|
|
87
80
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
expect(ActiveRecord::Base.connection_handler.active_connections?).to be(true)
|
|
91
|
-
subject.get_all
|
|
92
|
-
expect(ActiveRecord::Base.connection_handler.active_connections?).to be(true)
|
|
93
|
-
end
|
|
94
|
-
end
|
|
81
|
+
it 'should not poison wrapping transactions' do
|
|
82
|
+
flipper = Flipper.new(subject)
|
|
95
83
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
it "does not hold onto connections" do
|
|
100
|
-
expect(ActiveRecord::Base.connection_handler.active_connections?).to be(false)
|
|
101
|
-
subject.add(feature)
|
|
102
|
-
expect(ActiveRecord::Base.connection_handler.active_connections?).to be(false)
|
|
103
|
-
subject.remove(feature)
|
|
104
|
-
expect(ActiveRecord::Base.connection_handler.active_connections?).to be(false)
|
|
105
|
-
subject.clear(feature)
|
|
106
|
-
expect(ActiveRecord::Base.connection_handler.active_connections?).to be(false)
|
|
107
|
-
end
|
|
84
|
+
actor = Struct.new(:flipper_id).new('flipper-id-123')
|
|
85
|
+
flipper.enable_actor(:foo, actor)
|
|
108
86
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
subject.clear(feature)
|
|
117
|
-
expect(ActiveRecord::Base.connection_handler.active_connections?).to be(true)
|
|
118
|
-
end
|
|
119
|
-
end
|
|
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
|
|
120
94
|
|
|
121
|
-
|
|
122
|
-
|
|
95
|
+
context "ActiveRecord connection_pool" do
|
|
96
|
+
before do
|
|
97
|
+
clear_active_connections!
|
|
98
|
+
end
|
|
123
99
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
100
|
+
context "#features" do
|
|
101
|
+
it "does not hold onto connections" do
|
|
102
|
+
expect(active_connections?).to be(false)
|
|
103
|
+
subject.features
|
|
104
|
+
expect(active_connections?).to be(false)
|
|
105
|
+
end
|
|
129
106
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
107
|
+
it "does not release previously held connection" do
|
|
108
|
+
ActiveRecord::Base.connection # establish a new connection
|
|
109
|
+
expect(active_connections?).to be(true)
|
|
110
|
+
subject.features
|
|
111
|
+
expect(active_connections?).to be(true)
|
|
112
|
+
end
|
|
113
|
+
end
|
|
137
114
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
115
|
+
context "#get_all" do
|
|
116
|
+
it "does not hold onto connections" do
|
|
117
|
+
expect(active_connections?).to be(false)
|
|
118
|
+
subject.get_all
|
|
119
|
+
expect(active_connections?).to be(false)
|
|
120
|
+
end
|
|
141
121
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
122
|
+
it "does not release previously held connection" do
|
|
123
|
+
ActiveRecord::Base.connection # establish a new connection
|
|
124
|
+
expect(active_connections?).to be(true)
|
|
125
|
+
subject.get_all
|
|
126
|
+
expect(active_connections?).to be(true)
|
|
127
|
+
end
|
|
128
|
+
end
|
|
149
129
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
130
|
+
context "#add / #remove / #clear" do
|
|
131
|
+
let(:feature) { Flipper::Feature.new(:search, subject) }
|
|
132
|
+
|
|
133
|
+
it "does not hold onto connections" do
|
|
134
|
+
expect(active_connections?).to be(false)
|
|
135
|
+
subject.add(feature)
|
|
136
|
+
expect(active_connections?).to be(false)
|
|
137
|
+
subject.remove(feature)
|
|
138
|
+
expect(active_connections?).to be(false)
|
|
139
|
+
subject.clear(feature)
|
|
140
|
+
expect(active_connections?).to be(false)
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
it "does not release previously held connection" do
|
|
144
|
+
ActiveRecord::Base.connection # establish a new connection
|
|
145
|
+
expect(active_connections?).to be(true)
|
|
146
|
+
subject.add(feature)
|
|
147
|
+
expect(active_connections?).to be(true)
|
|
148
|
+
subject.remove(feature)
|
|
149
|
+
expect(active_connections?).to be(true)
|
|
150
|
+
subject.clear(feature)
|
|
151
|
+
expect(active_connections?).to be(true)
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
context "#get_multi" do
|
|
156
|
+
let(:feature) { Flipper::Feature.new(:search, subject) }
|
|
157
|
+
|
|
158
|
+
it "does not hold onto connections" do
|
|
159
|
+
expect(active_connections?).to be(false)
|
|
160
|
+
subject.get_multi([feature])
|
|
161
|
+
expect(active_connections?).to be(false)
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
it "does not release previously held connection" do
|
|
165
|
+
ActiveRecord::Base.connection # establish a new connection
|
|
166
|
+
expect(active_connections?).to be(true)
|
|
167
|
+
subject.get_multi([feature])
|
|
168
|
+
expect(active_connections?).to be(true)
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
context "#enable/#disable boolean" do
|
|
173
|
+
let(:feature) { Flipper::Feature.new(:search, subject) }
|
|
174
|
+
let(:gate) { feature.gate(:boolean)}
|
|
175
|
+
|
|
176
|
+
it "does not hold onto connections" do
|
|
177
|
+
expect(active_connections?).to be(false)
|
|
178
|
+
subject.enable(feature, gate, gate.wrap(true))
|
|
179
|
+
expect(active_connections?).to be(false)
|
|
180
|
+
subject.disable(feature, gate, gate.wrap(false))
|
|
181
|
+
expect(active_connections?).to be(false)
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
it "does not release previously held connection" do
|
|
185
|
+
ActiveRecord::Base.connection # establish a new connection
|
|
186
|
+
expect(active_connections?).to be(true)
|
|
187
|
+
subject.enable(feature, gate, gate.wrap(true))
|
|
188
|
+
expect(active_connections?).to be(true)
|
|
189
|
+
subject.disable(feature, gate, gate.wrap(false))
|
|
190
|
+
expect(active_connections?).to be(true)
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
context "#enable/#disable set" do
|
|
195
|
+
let(:feature) { Flipper::Feature.new(:search, subject) }
|
|
196
|
+
let(:gate) { feature.gate(:group) }
|
|
197
|
+
|
|
198
|
+
it "does not hold onto connections" do
|
|
199
|
+
expect(active_connections?).to be(false)
|
|
200
|
+
subject.enable(feature, gate, gate.wrap(:admin))
|
|
201
|
+
expect(active_connections?).to be(false)
|
|
202
|
+
subject.disable(feature, gate, gate.wrap(:admin))
|
|
203
|
+
expect(active_connections?).to be(false)
|
|
204
|
+
end
|
|
159
205
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
206
|
+
it "does not release previously held connection" do
|
|
207
|
+
ActiveRecord::Base.connection # establish a new connection
|
|
208
|
+
expect(active_connections?).to be(true)
|
|
209
|
+
subject.enable(feature, gate, gate.wrap(:admin))
|
|
210
|
+
expect(active_connections?).to be(true)
|
|
211
|
+
subject.disable(feature, gate, gate.wrap(:admin))
|
|
212
|
+
expect(active_connections?).to be(true)
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
end
|
|
163
216
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
217
|
+
if ActiveRecord.version >= Gem::Version.new('7.1')
|
|
218
|
+
context 'with read/write roles' do
|
|
219
|
+
before do
|
|
220
|
+
skip "connected_to with roles is not supported on #{config['adapter']}" if config["adapter"] == "sqlite3"
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
let(:abstract_class) do
|
|
224
|
+
# Create a named abstract class (Rails requires names for connects_to)
|
|
225
|
+
klass = Class.new(ActiveRecord::Base) do
|
|
226
|
+
self.abstract_class = true
|
|
227
|
+
end
|
|
228
|
+
stub_const('TestApplicationRecord', klass)
|
|
229
|
+
|
|
230
|
+
# Now configure connects_to with the same database for both roles
|
|
231
|
+
# In production, these would be different (primary/replica)
|
|
232
|
+
klass.connects_to database: {
|
|
233
|
+
writing: config,
|
|
234
|
+
reading: config
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
klass
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
after do
|
|
241
|
+
# Disconnect role-based connections to avoid interfering with database cleanup
|
|
242
|
+
clear_all_connections!
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
let(:feature_class) do
|
|
246
|
+
klass = Class.new(abstract_class) do
|
|
247
|
+
self.table_name = 'flipper_features'
|
|
248
|
+
validates :key, presence: true
|
|
249
|
+
end
|
|
250
|
+
stub_const('TestFeature', klass)
|
|
251
|
+
klass
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
let(:gate_class) do
|
|
255
|
+
klass = Class.new(abstract_class) do
|
|
256
|
+
self.table_name = 'flipper_gates'
|
|
257
|
+
end
|
|
258
|
+
stub_const('TestGate', klass)
|
|
259
|
+
klass
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
let(:adapter_with_roles) do
|
|
263
|
+
described_class.new(
|
|
264
|
+
feature_class: feature_class,
|
|
265
|
+
gate_class: gate_class
|
|
266
|
+
)
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
it 'can perform write operations when forced to reading role' do
|
|
270
|
+
abstract_class.connected_to(role: :reading) do
|
|
271
|
+
flipper = Flipper.new(adapter_with_roles)
|
|
272
|
+
|
|
273
|
+
feature = flipper[:test_feature]
|
|
274
|
+
expect { feature.enable }.not_to raise_error
|
|
275
|
+
expect(feature.enabled?).to be(true)
|
|
276
|
+
expect { feature.disable }.not_to raise_error
|
|
277
|
+
expect(feature.enabled?).to be(false)
|
|
278
|
+
|
|
279
|
+
feature = flipper[:actor_test]
|
|
280
|
+
actor = Struct.new(:flipper_id).new(123)
|
|
281
|
+
expect { feature.enable_actor(actor) }.not_to raise_error
|
|
282
|
+
expect(feature.enabled?(actor)).to be(true)
|
|
283
|
+
expect { feature.disable_actor(actor) }.not_to raise_error
|
|
284
|
+
expect(feature.enabled?(actor)).to be(false)
|
|
285
|
+
|
|
286
|
+
feature = flipper[:gate_test]
|
|
287
|
+
expect { feature.enable_percentage_of_time(50) }.not_to raise_error
|
|
288
|
+
expect { feature.disable_percentage_of_time }.not_to raise_error
|
|
289
|
+
feature.enable
|
|
290
|
+
expect { feature.remove }.not_to raise_error
|
|
291
|
+
|
|
292
|
+
feature = flipper[:expression_test]
|
|
293
|
+
expression = Flipper.property(:plan).eq("premium")
|
|
294
|
+
expect { feature.enable_expression(expression) }.not_to raise_error
|
|
295
|
+
expect(feature.expression).to eq(expression)
|
|
296
|
+
expect { feature.disable_expression }.not_to raise_error
|
|
297
|
+
expect(feature.expression).to be_nil
|
|
298
|
+
end
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
it 'does not hold onto connections during write operations' do
|
|
302
|
+
clear_active_connections!
|
|
303
|
+
|
|
304
|
+
abstract_class.connected_to(role: :reading) do
|
|
305
|
+
flipper = Flipper.new(adapter_with_roles)
|
|
306
|
+
feature = flipper[:connection_test]
|
|
307
|
+
|
|
308
|
+
feature.enable
|
|
309
|
+
expect(active_connections?).to be(false)
|
|
310
|
+
end
|
|
311
|
+
end
|
|
312
|
+
end
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
context 'requiring "flipper-active_record"' do
|
|
316
|
+
before do
|
|
317
|
+
Flipper.configuration = nil
|
|
318
|
+
Flipper.instance = nil
|
|
319
|
+
|
|
320
|
+
silence { load 'flipper/adapters/active_record.rb' }
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
it 'configures itself' do
|
|
324
|
+
expect(Flipper.adapter.adapter).to be_a(Flipper::Adapters::ActiveRecord)
|
|
325
|
+
end
|
|
326
|
+
end
|
|
170
327
|
end
|
|
171
328
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
329
|
+
context "without tables created" do
|
|
330
|
+
before(:all) do
|
|
331
|
+
skip_on_error(ActiveRecord::ConnectionNotEstablished, "#{config['adapter']} not available") do
|
|
332
|
+
silence do
|
|
333
|
+
ActiveRecord::Tasks::DatabaseTasks.create(config)
|
|
334
|
+
end
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
Flipper.configuration = nil
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
before(:each) do
|
|
341
|
+
ActiveRecord::Base.establish_connection(config)
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
after(:each) do
|
|
345
|
+
ActiveRecord::Base.connection.close
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
after(:all) do
|
|
349
|
+
silence { ActiveRecord::Tasks::DatabaseTasks.drop(config) } unless $skip
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
it "does not raise an error" do
|
|
353
|
+
Flipper.configuration = nil
|
|
354
|
+
Flipper.instance = nil
|
|
355
|
+
|
|
356
|
+
silence do
|
|
357
|
+
expect {
|
|
358
|
+
load 'flipper/adapters/active_record.rb'
|
|
359
|
+
Flipper::Adapters::ActiveRecord.new
|
|
360
|
+
}.not_to raise_error
|
|
361
|
+
end
|
|
362
|
+
end
|
|
179
363
|
end
|
|
180
364
|
end
|
|
181
365
|
end
|
|
366
|
+
|
|
367
|
+
def active_connections?
|
|
368
|
+
method = ActiveRecord::Base.connection_handler.method(:active_connections?)
|
|
369
|
+
method.arity == 0 ? method.call : method.call(:all)
|
|
370
|
+
end
|
|
371
|
+
|
|
372
|
+
def clear_active_connections!
|
|
373
|
+
method = ActiveRecord::Base.connection_handler.method(:clear_active_connections!)
|
|
374
|
+
method.arity == 0 ? method.call : method.call(:all)
|
|
375
|
+
end
|
|
376
|
+
|
|
377
|
+
def clear_all_connections!
|
|
378
|
+
method = ActiveRecord::Base.connection_handler.method(:clear_all_connections!)
|
|
379
|
+
method.arity == 0 ? method.call : method.call(:all)
|
|
380
|
+
end
|
|
182
381
|
end
|
|
@@ -11,8 +11,6 @@ class ActiveRecordTest < MiniTest::Test
|
|
|
11
11
|
database: ':memory:')
|
|
12
12
|
|
|
13
13
|
def setup
|
|
14
|
-
@adapter = Flipper::Adapters::ActiveRecord.new
|
|
15
|
-
|
|
16
14
|
ActiveRecord::Base.connection.execute <<-SQL
|
|
17
15
|
CREATE TABLE flipper_features (
|
|
18
16
|
id integer PRIMARY KEY,
|
|
@@ -36,6 +34,8 @@ class ActiveRecordTest < MiniTest::Test
|
|
|
36
34
|
ActiveRecord::Base.connection.execute <<-SQL
|
|
37
35
|
CREATE UNIQUE INDEX index_gates_on_keys_and_value on flipper_gates (feature_key, key, value)
|
|
38
36
|
SQL
|
|
37
|
+
|
|
38
|
+
@adapter = Flipper::Adapters::ActiveRecord.new
|
|
39
39
|
end
|
|
40
40
|
|
|
41
41
|
def teardown
|
|
@@ -47,9 +47,16 @@ class ActiveRecordTest < MiniTest::Test
|
|
|
47
47
|
ActiveRecord::Base.table_name_prefix = :foo_
|
|
48
48
|
ActiveRecord::Base.table_name_suffix = :_bar
|
|
49
49
|
|
|
50
|
-
|
|
51
|
-
Flipper::Adapters::ActiveRecord.send(:remove_const, :
|
|
50
|
+
# Remove constants so they get redefined with new prefix/suffix
|
|
51
|
+
Flipper::Adapters::ActiveRecord.send(:remove_const, :Model) if Flipper::Adapters::ActiveRecord.const_defined?(:Model)
|
|
52
|
+
Flipper::Adapters::ActiveRecord.send(:remove_const, :Feature) if Flipper::Adapters::ActiveRecord.const_defined?(:Feature)
|
|
53
|
+
Flipper::Adapters::ActiveRecord.send(:remove_const, :Gate) if Flipper::Adapters::ActiveRecord.const_defined?(:Gate)
|
|
54
|
+
Flipper::Adapters.send(:remove_const, :ActiveRecord)
|
|
55
|
+
|
|
52
56
|
load("flipper/adapters/active_record.rb")
|
|
57
|
+
load("flipper/adapters/active_record/model.rb")
|
|
58
|
+
load("flipper/adapters/active_record/feature.rb")
|
|
59
|
+
load("flipper/adapters/active_record/gate.rb")
|
|
53
60
|
|
|
54
61
|
assert_equal "foo_flipper_features_bar", Flipper::Adapters::ActiveRecord::Feature.table_name
|
|
55
62
|
assert_equal "foo_flipper_gates_bar", Flipper::Adapters::ActiveRecord::Gate.table_name
|
|
@@ -58,8 +65,15 @@ class ActiveRecordTest < MiniTest::Test
|
|
|
58
65
|
ActiveRecord::Base.table_name_prefix = ""
|
|
59
66
|
ActiveRecord::Base.table_name_suffix = ""
|
|
60
67
|
|
|
61
|
-
|
|
62
|
-
Flipper::Adapters::ActiveRecord.send(:remove_const, :
|
|
68
|
+
# Remove constants so they get redefined with reset prefix/suffix
|
|
69
|
+
Flipper::Adapters::ActiveRecord.send(:remove_const, :Model) if Flipper::Adapters::ActiveRecord.const_defined?(:Model)
|
|
70
|
+
Flipper::Adapters::ActiveRecord.send(:remove_const, :Feature) if Flipper::Adapters::ActiveRecord.const_defined?(:Feature)
|
|
71
|
+
Flipper::Adapters::ActiveRecord.send(:remove_const, :Gate) if Flipper::Adapters::ActiveRecord.const_defined?(:Gate)
|
|
72
|
+
Flipper::Adapters.send(:remove_const, :ActiveRecord)
|
|
73
|
+
|
|
63
74
|
load("flipper/adapters/active_record.rb")
|
|
75
|
+
load("flipper/adapters/active_record/model.rb")
|
|
76
|
+
load("flipper/adapters/active_record/feature.rb")
|
|
77
|
+
load("flipper/adapters/active_record/gate.rb")
|
|
64
78
|
end
|
|
65
79
|
end
|
|
@@ -1,6 +1,4 @@
|
|
|
1
1
|
require 'helper'
|
|
2
|
-
require 'active_record'
|
|
3
|
-
require 'rails/generators/test_case'
|
|
4
2
|
require 'generators/flipper/active_record_generator'
|
|
5
3
|
|
|
6
4
|
class FlipperActiveRecordGeneratorTest < Rails::Generators::TestCase
|
|
@@ -17,7 +15,7 @@ class FlipperActiveRecordGeneratorTest < Rails::Generators::TestCase
|
|
|
17
15
|
end
|
|
18
16
|
assert_migration 'db/migrate/create_flipper_tables.rb', <<~MIGRATION
|
|
19
17
|
class CreateFlipperTables < ActiveRecord::Migration#{migration_version}
|
|
20
|
-
def
|
|
18
|
+
def up
|
|
21
19
|
create_table :flipper_features do |t|
|
|
22
20
|
t.string :key, null: false
|
|
23
21
|
t.timestamps null: false
|
|
@@ -27,13 +25,13 @@ class FlipperActiveRecordGeneratorTest < Rails::Generators::TestCase
|
|
|
27
25
|
create_table :flipper_gates do |t|
|
|
28
26
|
t.string :feature_key, null: false
|
|
29
27
|
t.string :key, null: false
|
|
30
|
-
t.
|
|
28
|
+
t.text :value
|
|
31
29
|
t.timestamps null: false
|
|
32
30
|
end
|
|
33
|
-
add_index :flipper_gates, [:feature_key, :key, :value], unique: true
|
|
31
|
+
add_index :flipper_gates, [:feature_key, :key, :value], unique: true, length: { value: 255 }
|
|
34
32
|
end
|
|
35
33
|
|
|
36
|
-
def
|
|
34
|
+
def down
|
|
37
35
|
drop_table :flipper_gates
|
|
38
36
|
drop_table :flipper_features
|
|
39
37
|
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: flipper-active_record
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- John Nunemaker
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date:
|
|
11
|
+
date: 2026-02-26 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: flipper
|
|
@@ -16,14 +16,14 @@ dependencies:
|
|
|
16
16
|
requirements:
|
|
17
17
|
- - "~>"
|
|
18
18
|
- !ruby/object:Gem::Version
|
|
19
|
-
version: 1.
|
|
19
|
+
version: 1.4.0
|
|
20
20
|
type: :runtime
|
|
21
21
|
prerelease: false
|
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
|
23
23
|
requirements:
|
|
24
24
|
- - "~>"
|
|
25
25
|
- !ruby/object:Gem::Version
|
|
26
|
-
version: 1.
|
|
26
|
+
version: 1.4.0
|
|
27
27
|
- !ruby/object:Gem::Dependency
|
|
28
28
|
name: activerecord
|
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -33,7 +33,7 @@ dependencies:
|
|
|
33
33
|
version: '4.2'
|
|
34
34
|
- - "<"
|
|
35
35
|
- !ruby/object:Gem::Version
|
|
36
|
-
version: '
|
|
36
|
+
version: '9'
|
|
37
37
|
type: :runtime
|
|
38
38
|
prerelease: false
|
|
39
39
|
version_requirements: !ruby/object:Gem::Requirement
|
|
@@ -43,7 +43,7 @@ dependencies:
|
|
|
43
43
|
version: '4.2'
|
|
44
44
|
- - "<"
|
|
45
45
|
- !ruby/object:Gem::Version
|
|
46
|
-
version: '
|
|
46
|
+
version: '9'
|
|
47
47
|
description:
|
|
48
48
|
email: support@flippercloud.io
|
|
49
49
|
executables: []
|
|
@@ -56,10 +56,14 @@ files:
|
|
|
56
56
|
- examples/active_record/ar_setup.rb
|
|
57
57
|
- examples/active_record/basic.rb
|
|
58
58
|
- examples/active_record/cached.rb
|
|
59
|
+
- examples/active_record/group_migration.rb
|
|
59
60
|
- examples/active_record/internals.rb
|
|
60
61
|
- flipper-active_record.gemspec
|
|
61
62
|
- lib/flipper-active_record.rb
|
|
62
63
|
- lib/flipper/adapters/active_record.rb
|
|
64
|
+
- lib/flipper/adapters/active_record/feature.rb
|
|
65
|
+
- lib/flipper/adapters/active_record/gate.rb
|
|
66
|
+
- lib/flipper/adapters/active_record/model.rb
|
|
63
67
|
- lib/flipper/version.rb
|
|
64
68
|
- lib/generators/flipper/active_record_generator.rb
|
|
65
69
|
- lib/generators/flipper/templates/migration.erb
|
|
@@ -74,7 +78,8 @@ metadata:
|
|
|
74
78
|
homepage_uri: https://www.flippercloud.io
|
|
75
79
|
source_code_uri: https://github.com/flippercloud/flipper
|
|
76
80
|
bug_tracker_uri: https://github.com/flippercloud/flipper/issues
|
|
77
|
-
changelog_uri: https://github.com/flippercloud/flipper/
|
|
81
|
+
changelog_uri: https://github.com/flippercloud/flipper/releases/tag/v1.4.0
|
|
82
|
+
funding_uri: https://github.com/sponsors/flippercloud
|
|
78
83
|
post_install_message:
|
|
79
84
|
rdoc_options: []
|
|
80
85
|
require_paths:
|
|
@@ -90,10 +95,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
90
95
|
- !ruby/object:Gem::Version
|
|
91
96
|
version: '0'
|
|
92
97
|
requirements: []
|
|
93
|
-
rubygems_version: 3.
|
|
98
|
+
rubygems_version: 3.5.22
|
|
94
99
|
signing_key:
|
|
95
100
|
specification_version: 4
|
|
96
|
-
summary: ActiveRecord adapter for Flipper
|
|
97
|
-
test_files:
|
|
98
|
-
- spec/flipper/adapters/active_record_spec.rb
|
|
99
|
-
- test/adapters/active_record_test.rb
|
|
101
|
+
summary: ActiveRecord feature flag adapter for Flipper
|
|
102
|
+
test_files: []
|