* Renamed concern from Feature to Featurable * Feature: Installation config (#839) * Added new model installtion config with corresponding migrations and specs * Created an installation config yml (key value store model) * Created a config loader module to load the installaltion configs * Added this to the config loader seeder * Changed the account before create hook for default feature enabling to use the feature values from installtion config * Renamed the feature concern to Featurable to follow the naming pattern for concerns * Added comments and specs for modules and places that deemed necessary * Refactored config loader to reduce cognitive complexity (#839)
This commit is contained in:
parent
76b98cbed4
commit
905c93b8f8
11 changed files with 203 additions and 18 deletions
|
@ -94,6 +94,8 @@ Rails/UniqueValidationWithoutIndex:
|
|||
Exclude:
|
||||
- 'app/models/channel/twitter_profile.rb'
|
||||
- 'app/models/webhook.rb'
|
||||
RSpec/NamedSubject:
|
||||
Enabled: false
|
||||
AllCops:
|
||||
Exclude:
|
||||
- 'bin/**/*'
|
||||
|
|
|
@ -19,7 +19,7 @@ class Account < ApplicationRecord
|
|||
|
||||
include Events::Types
|
||||
include Reportable
|
||||
include Features
|
||||
include Featurable
|
||||
|
||||
DEFAULT_QUERY_SETTING = {
|
||||
flag_query_mode: :bit_operator
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
module Features
|
||||
module Featurable
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
QUERY_MODE = {
|
||||
|
@ -51,7 +51,10 @@ module Features
|
|||
private
|
||||
|
||||
def enable_default_features
|
||||
features_to_enabled = FEATURE_LIST.select { |f| f['enabled'] }.map { |f| f['name'] }
|
||||
config = InstallationConfig.find_by(name: 'ACCOUNT_LEVEL_FEATURE_DEFAULTS')
|
||||
return true if config.blank?
|
||||
|
||||
features_to_enabled = config.value.select { |f| f[:enabled] }.map { |f| f[:name] }
|
||||
enable_features(features_to_enabled)
|
||||
end
|
||||
end
|
31
app/models/installation_config.rb
Normal file
31
app/models/installation_config.rb
Normal file
|
@ -0,0 +1,31 @@
|
|||
# == Schema Information
|
||||
#
|
||||
# Table name: installation_configs
|
||||
#
|
||||
# id :bigint not null, primary key
|
||||
# name :string not null
|
||||
# serialized_value :jsonb not null
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
#
|
||||
# Indexes
|
||||
#
|
||||
# index_installation_configs_on_name_and_created_at (name,created_at) UNIQUE
|
||||
#
|
||||
class InstallationConfig < ApplicationRecord
|
||||
serialize :serialized_value, HashWithIndifferentAccess
|
||||
|
||||
validates :name, presence: true
|
||||
|
||||
default_scope { order(created_at: :desc) }
|
||||
|
||||
def value
|
||||
serialized_value[:value]
|
||||
end
|
||||
|
||||
def value=(value_to_assigned)
|
||||
self.serialized_value = {
|
||||
value: value_to_assigned
|
||||
}.with_indifferent_access
|
||||
end
|
||||
end
|
2
config/installation_config.yml
Normal file
2
config/installation_config.yml
Normal file
|
@ -0,0 +1,2 @@
|
|||
- name: SHOW_WIDGET_HEADER
|
||||
value: true
|
13
db/migrate/20200510112339_create_installation_config.rb
Normal file
13
db/migrate/20200510112339_create_installation_config.rb
Normal file
|
@ -0,0 +1,13 @@
|
|||
class CreateInstallationConfig < ActiveRecord::Migration[6.0]
|
||||
def change
|
||||
create_table :installation_configs do |t|
|
||||
t.string :name, null: false
|
||||
t.jsonb :serialized_value, null: false, default: '{}'
|
||||
t.timestamps
|
||||
end
|
||||
|
||||
add_index :installation_configs, [:name, :created_at], unique: true
|
||||
|
||||
ConfigLoader.new.process
|
||||
end
|
||||
end
|
24
db/schema.rb
24
db/schema.rb
|
@ -10,7 +10,7 @@
|
|||
#
|
||||
# It's strongly recommended that you check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema.define(version: 2020_05_09_044639) do
|
||||
ActiveRecord::Schema.define(version: 2020_05_10_112339) do
|
||||
|
||||
# These are extensions that must be enabled in order to support this database
|
||||
enable_extension "pgcrypto"
|
||||
|
@ -246,6 +246,14 @@ ActiveRecord::Schema.define(version: 2020_05_09_044639) do
|
|||
t.index ["account_id"], name: "index_inboxes_on_account_id"
|
||||
end
|
||||
|
||||
create_table "installation_configs", force: :cascade do |t|
|
||||
t.string "name", null: false
|
||||
t.jsonb "serialized_value", default: "{}", null: false
|
||||
t.datetime "created_at", precision: 6, null: false
|
||||
t.datetime "updated_at", precision: 6, null: false
|
||||
t.index ["name", "created_at"], name: "index_installation_configs_on_name_and_created_at", unique: true
|
||||
end
|
||||
|
||||
create_table "messages", id: :serial, force: :cascade do |t|
|
||||
t.text "content"
|
||||
t.integer "account_id", null: false
|
||||
|
@ -319,20 +327,6 @@ ActiveRecord::Schema.define(version: 2020_05_09_044639) do
|
|||
t.boolean "payment_source_added", default: false
|
||||
end
|
||||
|
||||
create_table "super_admins", force: :cascade do |t|
|
||||
t.string "email", default: "", null: false
|
||||
t.string "encrypted_password", default: "", null: false
|
||||
t.datetime "remember_created_at"
|
||||
t.integer "sign_in_count", default: 0, null: false
|
||||
t.datetime "current_sign_in_at"
|
||||
t.datetime "last_sign_in_at"
|
||||
t.inet "current_sign_in_ip"
|
||||
t.inet "last_sign_in_ip"
|
||||
t.datetime "created_at", precision: 6, null: false
|
||||
t.datetime "updated_at", precision: 6, null: false
|
||||
t.index ["email"], name: "index_super_admins_on_email", unique: true
|
||||
end
|
||||
|
||||
create_table "taggings", id: :serial, force: :cascade do |t|
|
||||
t.integer "tag_id"
|
||||
t.string "taggable_type"
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
# loading installation configs
|
||||
ConfigLoader.new.process
|
||||
|
||||
account = Account.create!(
|
||||
name: 'Acme Inc',
|
||||
domain: 'support.chatwoot.com',
|
||||
|
|
84
lib/config_loader.rb
Normal file
84
lib/config_loader.rb
Normal file
|
@ -0,0 +1,84 @@
|
|||
class ConfigLoader
|
||||
DEFAULT_OPTIONS = {
|
||||
config_path: nil,
|
||||
reconcile_only_new: true
|
||||
}.freeze
|
||||
|
||||
def process(options = {})
|
||||
options = DEFAULT_OPTIONS.merge(options)
|
||||
# function of the "reconcile_only_new" flag
|
||||
# if true,
|
||||
# it leaves the existing config and feature flags as it is and
|
||||
# creates the missing configs and feature flags with their default values
|
||||
# if false,
|
||||
# then it overwrites existing config and feature flags with default values
|
||||
# also creates the missing configs and feature flags with their default values
|
||||
@reconcile_only_new = options[:reconcile_only_new]
|
||||
|
||||
# setting the config path
|
||||
@config_path = options[:config_path].presence
|
||||
@config_path ||= Rails.root.join('config')
|
||||
|
||||
# general installation configs
|
||||
reconcile_general_config
|
||||
|
||||
# default account based feature configs
|
||||
reconcile_feature_config
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def general_configs
|
||||
@general_configs ||= YAML.safe_load(File.read("#{@config_path}/installation_config.yml")).freeze
|
||||
end
|
||||
|
||||
def account_features
|
||||
@account_features ||= YAML.safe_load(File.read("#{@config_path}/features.yml")).freeze
|
||||
end
|
||||
|
||||
def reconcile_general_config
|
||||
general_configs.each do |config|
|
||||
new_config = config.with_indifferent_access
|
||||
existing_config = InstallationConfig.find_by(name: new_config[:name])
|
||||
save_general_config(existing_config, new_config)
|
||||
end
|
||||
end
|
||||
|
||||
def save_general_config(existing_config, new_config)
|
||||
if existing_config
|
||||
# save config only if reconcile flag is false and existing configs value does not match default value
|
||||
save_as_new_config(new_config) if !@reconcile_only_new && existing_config.value != new_config[:value]
|
||||
else
|
||||
save_as_new_config(new_config)
|
||||
end
|
||||
end
|
||||
|
||||
def save_as_new_config(new_config)
|
||||
config = InstallationConfig.new(name: new_config[:name])
|
||||
config.value = new_config[:value]
|
||||
config.save
|
||||
end
|
||||
|
||||
def reconcile_feature_config
|
||||
config = InstallationConfig.find_by(name: 'ACCOUNT_LEVEL_FEATURE_DEFAULTS')
|
||||
|
||||
if config
|
||||
return false if config.value.to_s == account_features.to_s
|
||||
|
||||
compare_and_save(config)
|
||||
else
|
||||
save_as_new_config({ name: 'ACCOUNT_LEVEL_FEATURE_DEFAULTS', value: account_features })
|
||||
end
|
||||
end
|
||||
|
||||
def compare_and_save_feature(config)
|
||||
features = if @reconcile_only_new
|
||||
# leave the existing feature flag values as it is and add new feature flags with default values
|
||||
(account_features + config.value).uniq { |h| h['name'] }
|
||||
else
|
||||
# update the existing feature flag values with default values and add new feature flags with default values
|
||||
(config.value + account_features).uniq { |h| h['name'] }
|
||||
end
|
||||
save_as_new_config({ name: 'ACCOUNT_LEVEL_FEATURE_DEFAULTS', value: features })
|
||||
end
|
||||
end
|
46
spec/lib/config_loader_spec.rb
Normal file
46
spec/lib/config_loader_spec.rb
Normal file
|
@ -0,0 +1,46 @@
|
|||
require 'rails_helper'
|
||||
|
||||
describe ConfigLoader do
|
||||
subject(:trigger) { described_class.new.process }
|
||||
|
||||
describe 'execute' do
|
||||
context 'when called with default options' do
|
||||
it 'creates installation configs' do
|
||||
expect(InstallationConfig.count).to eq(0)
|
||||
subject
|
||||
expect(InstallationConfig.count).to be > 0
|
||||
end
|
||||
|
||||
it 'creates account level feature defaults as entry on config table' do
|
||||
subject
|
||||
expect(InstallationConfig.find_by(name: 'ACCOUNT_LEVEL_FEATURE_DEFAULTS')).to be_truthy
|
||||
end
|
||||
end
|
||||
|
||||
context 'with reconcile_only_new option' do
|
||||
let(:class_instance) { described_class.new }
|
||||
let(:config) { { name: 'WHO', value: 'corona' } }
|
||||
let(:updated_config) { { name: 'WHO', value: 'covid 19' } }
|
||||
|
||||
before do
|
||||
allow(described_class).to receive(:new).and_return(class_instance)
|
||||
allow(class_instance).to receive(:general_configs).and_return([config])
|
||||
described_class.new.process
|
||||
end
|
||||
|
||||
it 'being true it should not update existing config value' do
|
||||
expect(InstallationConfig.find_by(name: 'WHO').value).to eq('corona')
|
||||
allow(class_instance).to receive(:general_configs).and_return([updated_config])
|
||||
described_class.new.process({ reconcile_only_new: true })
|
||||
expect(InstallationConfig.find_by(name: 'WHO').value).to eq('corona')
|
||||
end
|
||||
|
||||
it 'updates the existing config value with new default value' do
|
||||
expect(InstallationConfig.find_by(name: 'WHO').value).to eq('corona')
|
||||
allow(class_instance).to receive(:general_configs).and_return([updated_config])
|
||||
described_class.new.process({ reconcile_only_new: false })
|
||||
expect(InstallationConfig.find_by(name: 'WHO').value).to eq('covid 19')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
7
spec/models/installation_config_spec.rb
Normal file
7
spec/models/installation_config_spec.rb
Normal file
|
@ -0,0 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe InstallationConfig do
|
||||
it { is_expected.to validate_presence_of(:name) }
|
||||
end
|
Loading…
Reference in a new issue