bp3-core 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,139 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bp3
4
+ module Core
5
+ module Cookies
6
+ extend ActiveSupport::Concern
7
+
8
+ included do
9
+ attr_reader :current_visitor
10
+
11
+ before_action :check_visitor_cookie
12
+ end
13
+
14
+ VISITOR_COOKIE_NAME_PREFIX = '_bp3_visitor'
15
+ DO_NOT_TRACK_VALUE = 'do_not_track'
16
+
17
+ private
18
+
19
+ def do_not_track
20
+ return @do_not_track unless @do_not_track.nil? # could be set to true or false already
21
+
22
+ cookie_value = cookies[new_visitor_cookie_name]
23
+ @do_not_track = cookie_value == DO_NOT_TRACK_VALUE
24
+ reset_session if @do_not_track
25
+ @do_not_track
26
+ end
27
+
28
+ def set_do_not_track
29
+ # this replaces the signed temporary cookie with an unsigned, permanent cookie
30
+ cookies.permanent[new_visitor_cookie_name] = DO_NOT_TRACK_VALUE
31
+ end
32
+
33
+ def start_tracking
34
+ return unless do_not_track
35
+
36
+ cookies.delete(visitor_cookie_name)
37
+ @do_not_track = false
38
+ check_visitor_cookie
39
+ end
40
+
41
+ def check_visitor_cookie
42
+ switch_old_to_new
43
+ check_new_visitor_cookie
44
+ end
45
+
46
+ def switch_old_to_new
47
+ cookie_value = cookies.signed[visitor_cookie_name]
48
+ return if cookie_value.blank?
49
+
50
+ _sites_site_id, _tenant_id, identification = cookie_value&.split('/')
51
+
52
+ message = "check_visitor_cookie: switching to new visitor_cookie for #{identification}"
53
+ Rails.logger.debug message
54
+ cookies.delete(visitor_cookie_name)
55
+ cookies.signed[new_visitor_cookie_name] = {
56
+ value: identification,
57
+ expires: 365.days.from_now
58
+ }
59
+ end
60
+
61
+ # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
62
+ def check_old_visitor_cookie
63
+ cookie_value = cookies.signed[visitor_cookie_name]
64
+ return if cookie_value.blank? && do_not_track
65
+
66
+ sites_site_id, tenant_id, identification = cookie_value&.split('/')
67
+ if sites_site_id && sites_site_id != GRS.current_site.id
68
+ Rails.logger.warn { "check_visitor_cookie: site mismatch! (#{sites_site_id} and #{GRS.current_site.id}" }
69
+ end
70
+ if tenant_id && tenant_id != GRS.current_tenant.id
71
+ Rails.logger.warn { "check_visitor_cookie: tenant mismatch! (#{tenant_id} and #{GRS.current_tenant.id}" }
72
+ end
73
+ visitor = Users::Visitor.find_by(sites_site_id:, tenant_id:, identification:)
74
+ if visitor.nil?
75
+ visitor = create_visitor
76
+ message = "check_visitor_cookie: create_visitor #{visitor.id} and create #{visitor_cookie_name} cookie"
77
+ Rails.logger.debug message
78
+ cookies.signed[visitor_cookie_name] = {
79
+ value: visitor.scoped_identification,
80
+ expires: 365.days.from_now
81
+ }
82
+ end
83
+ @current_visitor = GlobalRequestState.current_visitor = visitor
84
+ Rails.logger.debug do
85
+ "check_visitor_cookie: cookie[#{visitor_cookie_name}]=#{cookies.signed[visitor_cookie_name]}"
86
+ end
87
+ end
88
+ # rubocop:enable Metrics/AbcSize, Metrics/MethodLength
89
+
90
+ # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
91
+ def check_new_visitor_cookie
92
+ cookie_value = cookies.signed[new_visitor_cookie_name]
93
+ return if cookie_value.blank? && do_not_track
94
+
95
+ identification = cookie_value
96
+ sites_site_id = GRS.current_site_id
97
+ tenant_id = GRS.current_tenant_id
98
+ visitor = Users::Visitor.find_by(sites_site_id:, tenant_id:, identification:)
99
+ if visitor.nil?
100
+ visitor = create_visitor
101
+ message = "check_visitor_cookie: create_visitor #{visitor.id} and create #{new_visitor_cookie_name} cookie"
102
+ Rails.logger.debug message
103
+ cookies.signed[new_visitor_cookie_name] = {
104
+ value: visitor.identification,
105
+ expires: 365.days.from_now
106
+ }
107
+ end
108
+ @current_visitor = GlobalRequestState.current_visitor = visitor
109
+ Rails.logger.debug do
110
+ "check_visitor_cookie: cookie[#{new_visitor_cookie_name}]=#{cookies.signed[new_visitor_cookie_name]}"
111
+ end
112
+ end
113
+ # rubocop:enable Metrics/AbcSize, Metrics/MethodLength
114
+
115
+ def visitor_cookie_name
116
+ @visitor_cookie_name ||= "#{VISITOR_COOKIE_NAME_PREFIX}_#{cookie_site_id}_#{cookie_tenant_id}"
117
+ end
118
+
119
+ def new_visitor_cookie_name
120
+ VISITOR_COOKIE_NAME_PREFIX
121
+ end
122
+
123
+ def create_visitor
124
+ Users::Visitor.create!(sites_site: GRS.current_site,
125
+ tenant: GRS.current_tenant,
126
+ workspaces_workspace: GRS.current_workspace,
127
+ identification: SecureRandom.uuid)
128
+ end
129
+
130
+ def cookie_site_id
131
+ GRS.current_site.id[0..7]
132
+ end
133
+
134
+ def cookie_tenant_id
135
+ GRS.current_tenant.id[0..7]
136
+ end
137
+ end
138
+ end
139
+ end
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'action_view/record_identifier'
4
+ require 'active_support/parameter_filter'
5
+
6
+ module Bp3
7
+ module Core
8
+ module Displayable
9
+ extend ActiveSupport::Concern
10
+
11
+ def to_dom_id
12
+ ActionView::RecordIdentifier.dom_id(self)
13
+ end
14
+
15
+ def display_name
16
+ admin_display_name
17
+ end
18
+
19
+ def admin_display_name
20
+ "#{self.class.name.split('::').last} #{id}"
21
+ end
22
+
23
+ def filtered_attributes
24
+ filter_object_fields if respond_to?(:object)
25
+ filter_attributes
26
+ end
27
+
28
+ def i18n_key
29
+ self.class.i18n_key
30
+ end
31
+
32
+ private
33
+
34
+ def version_filter_mask
35
+ '[FILTERED][DC]'
36
+ end
37
+
38
+ def filter_attributes
39
+ filter.filter(attributes)
40
+ end
41
+
42
+ def filter_object_fields
43
+ self.object = filter.filter(object) if object.present?
44
+ return if object_changes.blank?
45
+
46
+ filtered_object_changes = filter.filter(object_changes)
47
+ filtered_object_changes.each_key do |key|
48
+ if filtered_object_changes[key] == version_filter_mask
49
+ filtered_object_changes[key] =
50
+ mark_changes_as_filtered(key)
51
+ end
52
+ end
53
+ self.object_changes = filtered_object_changes
54
+ end
55
+
56
+ def filter
57
+ return @filter if @filter
58
+
59
+ filters = Rails.application.config.filter_parameters
60
+ @filter = ActiveSupport::ParameterFilter.new(filters, mask: version_filter_mask)
61
+ end
62
+
63
+ def mark_changes_as_filtered(key)
64
+ change = object_changes[key]
65
+ change[0] = version_filter_mask if change[0].present?
66
+ change[1] = version_filter_mask if change[1].present?
67
+ change
68
+ end
69
+
70
+ class_methods do
71
+ def i18n_key
72
+ name.downcase.gsub('::', '/')
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bp3
4
+ module Core
5
+ module FeatureFlags
6
+ extend ActiveSupport::Concern
7
+
8
+ def efcfon?(flag, ref: nil, default: nil)
9
+ ef = efon(flag, ref:, default: nil)
10
+ cf = cfon(flag, ref:, default: nil)
11
+
12
+ if ef.nil? && cf.nil?
13
+ default || false
14
+ elsif ef.nil?
15
+ cf
16
+ elsif cf.nil?
17
+ ef
18
+ else
19
+ cf || ef
20
+ end
21
+ end
22
+
23
+ def efon(flag, ref: nil, default: nil)
24
+ Feature::Enabler.on(flag, enablable: ref, default:)
25
+ end
26
+
27
+ def efon?(flag, ref: nil, default: nil)
28
+ efon(flag, ref:, default:) || default || false
29
+ end
30
+
31
+ def cfon(flag, ref: nil, default: nil)
32
+ return default if ref.nil?
33
+
34
+ ref.configs&.[](flag)
35
+ end
36
+
37
+ def cfon?(flag, ref: nil, default: nil)
38
+ cfon(flag, ref:, default:) || default || false
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bp3
4
+ # Bp3::Ransackable provides class methods expected by models that use ransack
5
+ module Core
6
+ module Ransackable
7
+ extend ActiveSupport::Concern
8
+
9
+ mattr_accessor :attribute_exceptions, default: []
10
+ mattr_accessor :association_exceptions, default: []
11
+
12
+ class_methods do
13
+ def ransackable_fields(auth_object = nil)
14
+ fields =
15
+ ransackable_attributes(auth_object) +
16
+ ransackable_associations(auth_object) +
17
+ ransackable_scopes(auth_object)
18
+ fields.map(&:to_sym).uniq
19
+ end
20
+
21
+ def ransackable_attributes(_auth_object = nil)
22
+ except = attribute_exceptions.map(&:to_s)
23
+ column_names.map(&:to_s) - except
24
+ end
25
+
26
+ def ransackable_associations(_auth_object = nil)
27
+ except = association_exceptions.map(&:to_s)
28
+ reflect_on_all_associations.map(&:name).map(&:to_s) - except
29
+ end
30
+
31
+ def ransackable_scopes(_auth_object = nil)
32
+ []
33
+ end
34
+
35
+ def ransortable_attributes(auth_object = nil)
36
+ ransackable_attributes(auth_object)
37
+ end
38
+
39
+ private
40
+
41
+ def attribute_exceptions
42
+ Bp3::Core::Ransackable.attribute_exceptions
43
+ end
44
+
45
+ def association_exceptions
46
+ Bp3::Core::Ransackable.association_exceptions
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bp3
4
+ module Core
5
+ module Rqid
6
+ extend ActiveSupport::Concern
7
+
8
+ mattr_accessor :global_request_state_class_name, :global_request_state_method
9
+
10
+ def self.global_request_state_class
11
+ @@global_request_state_class ||= global_request_state_class_name.constantize # rubocop:disable Style/ClassVars
12
+ end
13
+
14
+ included do
15
+ before_create :set_rqid
16
+
17
+ # CAUTION: these are defined as belongs_to, therefore returning one record. However, it is possible
18
+ # that multiple such records exist
19
+ belongs_to :original_request, class_name: 'Inbound::Request',
20
+ foreign_key: :rqid, primary_key: :rqid, optional: true
21
+ belongs_to :original_response, class_name: 'Inbound::Response',
22
+ foreign_key: :rqid, primary_key: :rqid, optional: true
23
+ belongs_to :original_visit, class_name: 'Visit',
24
+ foreign_key: :rqid, primary_key: :rqid, optional: true
25
+ end
26
+
27
+ private
28
+
29
+ def set_rqid
30
+ return if rqid
31
+
32
+ self.rqid = rqid_from_global_state
33
+ end
34
+
35
+ def rqid_from_global_state
36
+ Bp3::Core::Rqid.global_request_state_class.send(Bp3::Core::Rqid.global_request_state_method)
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bp3
4
+ module Core
5
+ module Settings
6
+ extend ActiveSupport::Concern
7
+
8
+ def create_request_record_is_on?
9
+ efcfon?(:create_request_record, ref: current_site, default: true)
10
+ end
11
+
12
+ def multi_tenant_is_on?
13
+ efcfon?('multi-tenant', ref: current_site, default: false)
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bp3
4
+ module Core
5
+ module Sqnr
6
+ extend ActiveSupport::Concern
7
+
8
+ included do
9
+ scope :sqnr, -> { order(sqnr: :asc) }
10
+ scope :rnqs, -> { order(sqnr: :desc) }
11
+ end
12
+
13
+ class_methods do
14
+ def use_sqnr_for_ordering
15
+ self.implicit_order_column = :sqnr
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,143 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bp3
4
+ module Core
5
+ module Tenantable
6
+ extend ActiveSupport::Concern
7
+
8
+ included do
9
+ # call connection to make sure the db meta data has been loaded
10
+ ignore_notifications_string = (ENV['IGNORE_NOTIFICATIONS'] || '').strip
11
+ connection if ignore_notifications_string.empty?
12
+ end
13
+
14
+ private
15
+
16
+ def set_sites_site_id
17
+ return if sites_site_id || sites_site
18
+
19
+ self.sites_site_id = GlobalRequestState.either_site_id
20
+ end
21
+
22
+ def set_tenant_id
23
+ return if tenant_id || tenant
24
+
25
+ self.tenant_id = GlobalRequestState.either_tenant_id
26
+ end
27
+
28
+ def tenant_matches_site
29
+ tid = tenant_id || tenant&.id
30
+ return if tid.nil?
31
+
32
+ tenant ||= Tenant.find(tid)
33
+ return if (sites_site_id || sites_site&.id) == tenant.sites_site_id
34
+
35
+ errors.add(:tenant, :must_match_site)
36
+ end
37
+
38
+ def set_workspaces_workspace_id
39
+ return if workspaces_workspace_id || workspaces_workspace
40
+
41
+ self.workspaces_workspace_id = GlobalRequestState.either_workspace_id
42
+ end
43
+
44
+ def workspaces_workspace_matches_site
45
+ wid = workspaces_workspace_id || workspaces_workspace&.id
46
+ return if wid.nil?
47
+
48
+ workspaces_workspace ||= Workspaces::Workspace.find(wid)
49
+ return if (sites_site_id || sites_site&.id) == workspaces_workspace.sites_site_id
50
+
51
+ errors.add(:workspaces_workspace, :must_match_site)
52
+ end
53
+
54
+ # rubocop:disable: Metrics/BlockLength
55
+ class_methods do
56
+ def configure_tenancy(tenancy_configuration = {})
57
+ @tenancy_configuration = default_configuration.merge(tenancy_configuration)
58
+ may_belong_to_site
59
+ may_belong_to_tenant
60
+ may_belong_to_workspace
61
+ rescue ActiveRecord::StatementInvalid, PG::UndefinedTable => e
62
+ Rails.logger.error { "ERROR in configure_tenancy: #{e.message}" }
63
+ # log_exception(e) # infinite loop
64
+ end
65
+
66
+ def may_belong_to_site
67
+ column = columns.detect { |c| c.name == 'sites_site_id' }
68
+ return if column.nil?
69
+
70
+ @tenancy_configuration[:belongs_to_site] = true
71
+ optional = column.null
72
+ belongs_to(:sites_site, class_name: 'Sites::Site', optional:)
73
+ alias_method :site, :sites_site
74
+ alias_method :site=, :sites_site=
75
+
76
+ before_validation :set_sites_site_id
77
+
78
+ default_scope lambda {
79
+ site = GlobalRequestState.either_site
80
+ site = nil if GlobalRequestState.current_root
81
+ where(sites_site_id: site.id) if site
82
+ }
83
+ end
84
+
85
+ # rubocop:disable Metrics/MethodLength
86
+ def may_belong_to_tenant
87
+ column = columns.detect { |c| c.name == 'tenant_id' }
88
+ return if column.nil?
89
+
90
+ @tenancy_configuration[:belongs_to_tenant] = true
91
+ optional = column.null
92
+ belongs_to(:tenant, optional:)
93
+
94
+ before_validation :set_tenant_id
95
+
96
+ validate :tenant_matches_site
97
+
98
+ default_scope lambda {
99
+ site = GlobalRequestState.either_site
100
+ site = nil if GlobalRequestState.current_root
101
+ tenant = GlobalRequestState.either_tenant
102
+ tenant = nil if GlobalRequestState.either_admin
103
+ if site && tenant # for non admins (i.e. users)
104
+ where(sites_site_id: site.id, tenant_id: tenant.id)
105
+ elsif site # for site admins
106
+ where(sites_site_id: site.id)
107
+ elsif tenant
108
+ raise RuntimeError # where(tenant_id: tenant.id)
109
+ end
110
+ }
111
+ end
112
+ # rubocop:enable Metrics/MethodLength
113
+
114
+ def may_belong_to_workspace
115
+ column = columns.detect { |c| c.name == 'workspaces_workspace_id' }
116
+ return if column.nil?
117
+
118
+ @tenancy_configuration[:belongs_to_workspace] = true
119
+ optional = column.null
120
+ belongs_to(:workspaces_workspace, class_name: 'Workspaces::Workspace', optional:)
121
+ alias_method :workspace, :workspaces_workspace
122
+ alias_method :workspace=, :workspaces_workspace=
123
+
124
+ before_validation :set_workspaces_workspace_id
125
+
126
+ validate :workspaces_workspace_matches_site
127
+ end
128
+
129
+ def default_configuration
130
+ {
131
+ site_presence: :db,
132
+ tenant_presence: :db,
133
+ workspace_presence: :db,
134
+ site_source: :either_site,
135
+ tenant_source: :either_tenant,
136
+ workspace_source: :either_workspace
137
+ }
138
+ end
139
+ end
140
+ # rubocop:disable: Metrics/BlockLength
141
+ end
142
+ end
143
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bp3
4
+ module Core
5
+ # Bp3::Core::Test provides a convenience class for testing Bp3::Core
6
+ class Test
7
+ # to test Ransackable
8
+ include Ransackable
9
+
10
+ def self.column_names
11
+ %i[id name]
12
+ end
13
+
14
+ def self.reflect_on_all_associations
15
+ []
16
+ end
17
+
18
+ # to test Cookies
19
+ # first define this:
20
+ def self.before_action(_method_name); end
21
+ # then include Cookies
22
+ include Cookies
23
+
24
+ # to test Displayable
25
+ include Displayable
26
+
27
+ # to test FeatureFlags
28
+ include FeatureFlags
29
+
30
+ # to test Rqid
31
+ # first define this:
32
+ def self.before_create(_method_name); end
33
+ def self.belongs_to(_association, **options); end
34
+ def self.scope(_scope_name, _lambda); end
35
+ # then include Rqid
36
+ include Rqid
37
+
38
+ # to test Sqnr
39
+ include Sqnr
40
+
41
+ # to test Tenantable
42
+ # first define this:
43
+ def self.connection; end
44
+ # then include Tenantable
45
+ include Tenantable
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bp3
4
+ module Core
5
+ VERSION = '0.1.2'
6
+ end
7
+ end
data/lib/bp3/core.rb ADDED
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support/concern'
4
+ require 'active_support/core_ext/module/attribute_accessors'
5
+
6
+ require_relative 'core/actions'
7
+ require_relative 'core/cookies'
8
+ require_relative 'core/displayable'
9
+ require_relative 'core/feature_flags'
10
+ require_relative 'core/ransackable'
11
+ require_relative 'core/settings'
12
+ require_relative 'core/tenantable'
13
+ require_relative 'core/rqid'
14
+ require_relative 'core/sqnr'
15
+ require_relative 'core/version'
16
+
17
+ module Bp3
18
+ module Core
19
+ end
20
+ end
data/lib/bp3-core.rb ADDED
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bp3/core'
data/sig/bp3/core.rbs ADDED
@@ -0,0 +1,6 @@
1
+ module Bp3
2
+ module Core
3
+ VERSION: String
4
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
5
+ end
6
+ end