bp3-core 0.1.2

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.
@@ -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