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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f11ba9f2e7fa94ab06ddddfc753ccb1b398ffc19023ab6b2f5a980ccb55bd974
4
- data.tar.gz: 2566b337548372d8d080f732dbb2824b771f5c58e429ac350ceb0afb5455fea8
3
+ metadata.gz: d550530c982b783d5bb01547b0dd63e65ae28cf684baf5cab91f633b45546a6a
4
+ data.tar.gz: abe3da9138ef86cc0a7fecf5496d4ac78b036cfb9f8188b782debf9be8ab92ba
5
5
  SHA512:
6
- metadata.gz: 40e29c142239ff3a5f474f2f0dd6019d3bc60fcc4aaa76021715cf1dcb241c50c2d2dbf1e669cd5b2efcfa5f6740b40bea477988e25d6b5445ff4fcddd111173
7
- data.tar.gz: 6d9c313e0402d1bd197a1abc17a1aa930296f8f854c132773bebc08c6dbf5fab49a2978a4df196267ca0e29af8b5277868458ba4a7d7a35feb4efdcdb44ed1e0
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
- 2000.times do |i|
10
- flipper.enable_actor :foo, Flipper::Actor.new("User;#{i}")
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', '< 8'
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
@@ -0,0 +1,9 @@
1
+ module Flipper
2
+ module Adapters
3
+ class ActiveRecord
4
+ class Model < ::ActiveRecord::Base
5
+ self.abstract_class = true
6
+ end
7
+ end
8
+ end
9
+ 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
- # Abstract base class for internal models
11
- class Model < ::ActiveRecord::Base
12
- self.abstract_class = true
13
- end
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.all.map(&:key).to_set }
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
- with_connection(@feature_class) do
62
- # race condition, but add is only used by enable/disable which happen
63
- # super rarely, so it shouldn't matter in practice
64
- @feature_class.transaction do
65
- unless @feature_class.where(key: feature.key).first
66
- begin
67
- @feature_class.create! { |f| f.key = feature.key }
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
- with_connection(@feature_class) do
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
- with_connection(@gate_class) { @gate_class.where(feature_key: feature.key).destroy_all }
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 = @feature_class.connection.select_rows(rows_query)
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
- with_connection(@gate_class) do
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
- with_connection(@gate_class) do
202
- @gate_class.transaction do
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
- @gate_class.where(feature_key: feature.key, key: gate.key).destroy_all
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
- with_connection(@gate_class) do
223
- @gate_class.create! do |g|
224
- g.feature_key = feature.key
225
- g.key = gate.key
226
- g.value = thing.value.to_s
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 :integer
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
@@ -1,3 +1,13 @@
1
1
  module Flipper
2
- VERSION = '1.0.0'.freeze
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 self.up
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.string :value
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 self.down
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
- ActiveRecord::Base.establish_connection(adapter: 'sqlite3',
11
- database: ':memory:')
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
- before(:each) do
15
- ActiveRecord::Base.connection.execute <<-SQL
16
- CREATE TABLE flipper_features (
17
- id integer PRIMARY KEY,
18
- key text NOT NULL UNIQUE,
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
- after(:each) do
41
- ActiveRecord::Base.connection.execute("DROP table IF EXISTS `flipper_features`")
42
- ActiveRecord::Base.connection.execute("DROP table IF EXISTS `flipper_gates`")
43
- end
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
- it_should_behave_like 'a flipper adapter'
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
- context 'requiring "flipper-active_record"' do
48
- before do
49
- Flipper.configuration = nil
50
- Flipper.instance = nil
52
+ Flipper.configuration = nil
53
+ end
51
54
 
52
- load 'flipper/adapters/active_record.rb'
53
- ActiveSupport.run_load_hooks(:active_record, ActiveRecord::Base)
54
- end
55
+ before(:each) do
56
+ silence do
57
+ ActiveRecord::Tasks::DatabaseTasks.purge(config)
58
+ CreateFlipperTables.migrate(:up)
59
+ end
60
+ end
55
61
 
56
- it 'configures itself' do
57
- expect(Flipper.adapter.adapter).to be_a(Flipper::Adapters::ActiveRecord)
58
- end
59
- end
62
+ after(:all) do
63
+ silence { ActiveRecord::Tasks::DatabaseTasks.drop(config) } unless $skip
64
+ end
60
65
 
61
- context "ActiveRecord connection_pool" do
62
- before do
63
- ActiveRecord::Base.clear_active_connections!
64
- end
66
+ it_should_behave_like 'a flipper adapter'
65
67
 
66
- context "#features" do
67
- it "does not hold onto connections" do
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
- it "does not release previously held connection" do
74
- ActiveRecord::Base.connection # establish a new connection
75
- expect(ActiveRecord::Base.connection_handler.active_connections?).to be(true)
76
- subject.features
77
- expect(ActiveRecord::Base.connection_handler.active_connections?).to be(true)
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
- context "#get_all" do
82
- it "does not hold onto connections" do
83
- expect(ActiveRecord::Base.connection_handler.active_connections?).to be(false)
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
- it "does not release previously held connection" do
89
- ActiveRecord::Base.connection # establish a new connection
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
- context "#add / #remove / #clear" do
97
- let(:feature) { Flipper::Feature.new(:search, subject) }
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
- it "does not release previously held connection" do
110
- ActiveRecord::Base.connection # establish a new connection
111
- expect(ActiveRecord::Base.connection_handler.active_connections?).to be(true)
112
- subject.add(feature)
113
- expect(ActiveRecord::Base.connection_handler.active_connections?).to be(true)
114
- subject.remove(feature)
115
- expect(ActiveRecord::Base.connection_handler.active_connections?).to be(true)
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
- context "#get_multi" do
122
- let(:feature) { Flipper::Feature.new(:search, subject) }
95
+ context "ActiveRecord connection_pool" do
96
+ before do
97
+ clear_active_connections!
98
+ end
123
99
 
124
- it "does not hold onto connections" do
125
- expect(ActiveRecord::Base.connection_handler.active_connections?).to be(false)
126
- subject.get_multi([feature])
127
- expect(ActiveRecord::Base.connection_handler.active_connections?).to be(false)
128
- end
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
- it "does not release previously held connection" do
131
- ActiveRecord::Base.connection # establish a new connection
132
- expect(ActiveRecord::Base.connection_handler.active_connections?).to be(true)
133
- subject.get_multi([feature])
134
- expect(ActiveRecord::Base.connection_handler.active_connections?).to be(true)
135
- end
136
- end
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
- context "#enable/#disable boolean" do
139
- let(:feature) { Flipper::Feature.new(:search, subject) }
140
- let(:gate) { feature.gate(:boolean)}
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
- it "does not hold onto connections" do
143
- expect(ActiveRecord::Base.connection_handler.active_connections?).to be(false)
144
- subject.enable(feature, gate, gate.wrap(true))
145
- expect(ActiveRecord::Base.connection_handler.active_connections?).to be(false)
146
- subject.disable(feature, gate, gate.wrap(false))
147
- expect(ActiveRecord::Base.connection_handler.active_connections?).to be(false)
148
- end
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
- it "does not release previously held connection" do
151
- ActiveRecord::Base.connection # establish a new connection
152
- expect(ActiveRecord::Base.connection_handler.active_connections?).to be(true)
153
- subject.enable(feature, gate, gate.wrap(true))
154
- expect(ActiveRecord::Base.connection_handler.active_connections?).to be(true)
155
- subject.disable(feature, gate, gate.wrap(false))
156
- expect(ActiveRecord::Base.connection_handler.active_connections?).to be(true)
157
- end
158
- end
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
- context "#enable/#disable set" do
161
- let(:feature) { Flipper::Feature.new(:search, subject) }
162
- let(:gate) { feature.gate(:group) }
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
- it "does not hold onto connections" do
165
- expect(ActiveRecord::Base.connection_handler.active_connections?).to be(false)
166
- subject.enable(feature, gate, gate.wrap(:admin))
167
- expect(ActiveRecord::Base.connection_handler.active_connections?).to be(false)
168
- subject.disable(feature, gate, gate.wrap(:admin))
169
- expect(ActiveRecord::Base.connection_handler.active_connections?).to be(false)
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
- it "does not release previously held connection" do
173
- ActiveRecord::Base.connection # establish a new connection
174
- expect(ActiveRecord::Base.connection_handler.active_connections?).to be(true)
175
- subject.enable(feature, gate, gate.wrap(:admin))
176
- expect(ActiveRecord::Base.connection_handler.active_connections?).to be(true)
177
- subject.disable(feature, gate, gate.wrap(:admin))
178
- expect(ActiveRecord::Base.connection_handler.active_connections?).to be(true)
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
- Flipper::Adapters::ActiveRecord.send(:remove_const, :Feature)
51
- Flipper::Adapters::ActiveRecord.send(:remove_const, :Gate)
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
- Flipper::Adapters::ActiveRecord.send(:remove_const, :Feature)
62
- Flipper::Adapters::ActiveRecord.send(:remove_const, :Gate)
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 self.up
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.string :value
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 self.down
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.0.0
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: 2023-08-23 00:00:00.000000000 Z
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.0.0
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.0.0
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: '8'
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: '8'
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/blob/main/Changelog.md
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.4.10
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: []