panda_pal 5.10.1 → 5.12.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (107) hide show
  1. checksums.yaml +4 -4
  2. data/MIT-LICENSE +0 -0
  3. data/README.md +31 -21
  4. data/Rakefile +0 -0
  5. data/app/assets/config/panda_pal_manifest.js +0 -0
  6. data/app/assets/javascripts/panda_pal/application.js +0 -0
  7. data/app/assets/javascripts/panda_pal/lti.js +0 -0
  8. data/app/assets/stylesheets/panda_pal/application.css +0 -0
  9. data/app/assets/stylesheets/panda_pal/lti.css +0 -0
  10. data/app/controllers/panda_pal/api_call_controller.rb +0 -0
  11. data/app/controllers/panda_pal/application_controller.rb +0 -0
  12. data/app/controllers/panda_pal/lti_controller.rb +0 -0
  13. data/app/controllers/panda_pal/lti_v1_p0_controller.rb +0 -0
  14. data/app/controllers/panda_pal/lti_v1_p3_controller.rb +14 -0
  15. data/app/helpers/panda_pal/application_helper.rb +0 -0
  16. data/app/jobs/panda_pal/jobs/grade_passback_job.rb +0 -0
  17. data/app/lib/lti_xml/base_platform.rb +0 -0
  18. data/app/lib/lti_xml/bridge_platform.rb +0 -0
  19. data/app/lib/lti_xml/canvas_platform.rb +0 -0
  20. data/app/lib/panda_pal/launch_url_helpers.rb +0 -0
  21. data/app/lib/panda_pal/lti_jwt_validator.rb +0 -0
  22. data/app/models/panda_pal/api_call.rb +0 -0
  23. data/app/models/panda_pal/organization.rb +3 -39
  24. data/app/models/panda_pal/organization_concerns/multi_database_sharding.rb +22 -0
  25. data/app/models/panda_pal/organization_concerns/organization_builder.rb +0 -0
  26. data/app/models/panda_pal/organization_concerns/settings_validation.rb +0 -0
  27. data/app/models/panda_pal/organization_concerns/task_scheduling.rb +0 -0
  28. data/app/models/panda_pal/organization_concerns/tenant_handling.rb +54 -0
  29. data/app/models/panda_pal/panda_pal_record.rb +0 -0
  30. data/app/models/panda_pal/platform/canvas.rb +0 -0
  31. data/app/models/panda_pal/platform.rb +0 -0
  32. data/app/models/panda_pal/session.rb +28 -13
  33. data/app/views/layouts/panda_pal/application.html.erb +0 -0
  34. data/app/views/panda_pal/lti/launch.html.erb +0 -0
  35. data/app/views/panda_pal/lti_v1_p3/login.html.erb +0 -0
  36. data/app/views/panda_pal/partials/_auto_submit_form.html.erb +0 -0
  37. data/config/dev_lti_key.key +0 -0
  38. data/config/initializers/apartment.rb +202 -3
  39. data/config/routes.rb +0 -0
  40. data/db/migrate/20160412205931_create_panda_pal_organizations.rb +0 -0
  41. data/db/migrate/20160413135653_create_panda_pal_sessions.rb +0 -0
  42. data/db/migrate/20160425130344_add_panda_pal_organization_to_session.rb +0 -0
  43. data/db/migrate/20170106165533_add_salesforce_id_to_organizations.rb +0 -0
  44. data/db/migrate/20171205183457_encrypt_organization_settings.rb +0 -0
  45. data/db/migrate/20171205194657_remove_old_organization_settings.rb +0 -0
  46. data/db/migrate/20220721095653_create_panda_pal_api_calls.rb +0 -0
  47. data/lib/panda_pal/concerns/ability_helper.rb +0 -0
  48. data/lib/panda_pal/engine.rb +0 -2
  49. data/lib/panda_pal/helpers/console_helpers.rb +0 -0
  50. data/lib/panda_pal/helpers/controller_helper.rb +0 -0
  51. data/lib/panda_pal/helpers/misc_helper.rb +0 -0
  52. data/lib/panda_pal/helpers/route_helper.rb +0 -0
  53. data/lib/panda_pal/helpers/secure_headers.rb +0 -0
  54. data/lib/panda_pal/helpers/session_replacement.rb +0 -0
  55. data/lib/panda_pal/helpers.rb +0 -0
  56. data/lib/panda_pal/plugins.rb +0 -0
  57. data/lib/panda_pal/spec_helper.rb +82 -0
  58. data/lib/panda_pal/version.rb +1 -1
  59. data/lib/panda_pal.rb +1 -1
  60. data/lib/tasks/panda_pal_tasks.rake +0 -0
  61. data/panda_pal.gemspec +3 -3
  62. data/spec/controllers/panda_pal/api_call_controller_spec.rb +0 -0
  63. data/spec/core/apartment_multidb_spec.rb +48 -0
  64. data/spec/dummy/README.rdoc +0 -0
  65. data/spec/dummy/Rakefile +0 -0
  66. data/spec/dummy/app/assets/javascripts/application.js +0 -0
  67. data/spec/dummy/app/assets/stylesheets/application.css +0 -0
  68. data/spec/dummy/app/controllers/application_controller.rb +0 -0
  69. data/spec/dummy/app/helpers/application_helper.rb +0 -0
  70. data/spec/dummy/app/views/layouts/application.html.erb +0 -0
  71. data/spec/dummy/config/application.rb +0 -0
  72. data/spec/dummy/config/boot.rb +0 -0
  73. data/spec/dummy/config/database.yml +6 -2
  74. data/spec/dummy/config/environment.rb +0 -0
  75. data/spec/dummy/config/environments/development.rb +0 -0
  76. data/spec/dummy/config/environments/production.rb +0 -0
  77. data/spec/dummy/config/environments/test.rb +5 -0
  78. data/spec/dummy/config/initializers/backtrace_silencers.rb +0 -0
  79. data/spec/dummy/config/initializers/cookies_serializer.rb +0 -0
  80. data/spec/dummy/config/initializers/filter_parameter_logging.rb +0 -0
  81. data/spec/dummy/config/initializers/inflections.rb +0 -0
  82. data/spec/dummy/config/initializers/mime_types.rb +0 -0
  83. data/spec/dummy/config/initializers/session_store.rb +0 -0
  84. data/spec/dummy/config/initializers/wrap_parameters.rb +0 -0
  85. data/spec/dummy/config/locales/en.yml +0 -0
  86. data/spec/dummy/config/routes.rb +0 -0
  87. data/spec/dummy/config/secrets.yml +0 -0
  88. data/spec/dummy/config.ru +0 -0
  89. data/spec/dummy/db/schema.rb +0 -0
  90. data/spec/dummy/db/test2_schema.rb +18 -0
  91. data/spec/dummy/public/404.html +0 -0
  92. data/spec/dummy/public/422.html +0 -0
  93. data/spec/dummy/public/500.html +0 -0
  94. data/spec/dummy/public/favicon.ico +0 -0
  95. data/spec/factories/panda_pal_organizations.rb +0 -0
  96. data/spec/factories/panda_pal_sessions.rb +0 -0
  97. data/spec/models/panda_pal/api_call_spec.rb +1 -1
  98. data/spec/models/panda_pal/organization/settings_validation_spec.rb +4 -0
  99. data/spec/models/panda_pal/organization_spec.rb +0 -0
  100. data/spec/models/panda_pal/session_spec.rb +0 -0
  101. data/spec/rails_helper.rb +4 -0
  102. data/spec/spec_helper.rb +0 -1
  103. metadata +57 -50
  104. /data/spec/dummy/{bin → app/bin}/bundle +0 -0
  105. /data/spec/dummy/{bin → app/bin}/rails +0 -0
  106. /data/spec/dummy/{bin → app/bin}/rake +0 -0
  107. /data/spec/dummy/{bin → app/bin}/setup +0 -0
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b642b95a1dba629d9af0aa06b0efd5a504f8b056660860a739d1f3ef24878092
4
- data.tar.gz: e1f42715850b2677bf0a6266249470bf4708bddd296fd9a54cfcd09659eaac6d
3
+ metadata.gz: ab899936461dc59208ae6afef28264feda7a75927798e158ca1ee8381b2ea463
4
+ data.tar.gz: 20424ac6bef9bede8af9e58e8a1393267b0966f96cf56dd8ae8ed0e5b486420a
5
5
  SHA512:
6
- metadata.gz: e84736bc29c501d19d46e8add102fb926926dbcc1de9502ba08e462619d080374a3af30d83f3978675b733f0e20283ebeea5c27d2d09569eabea1f92504951fd
7
- data.tar.gz: d95f8fd1da243a09f58f3fb45b0d04d3ff547cd43dfc91663e204f5bb114aec8d03084514126f471cf521b64dd49d270ae82deff07674331231509818f1b8dc7
6
+ metadata.gz: 85041955aabe8255fcf0a4e3cbcfce3c55c690e0d66d74ac511d6b4d70825d796a62a18fe3ac3e0b5dc66e9ddc865c5b45f672940d92c15a1cc38a7ac350ad91
7
+ data.tar.gz: bcbe2513a4b4916af671f5a913036e0154dd474d510351f6325a2f264d03c1dac10adc4c48bc060c40faafdc798268664c48e2c32943dd4ed3d0dcda076b7ff3
data/MIT-LICENSE CHANGED
File without changes
data/README.md CHANGED
@@ -57,34 +57,32 @@ Use one of these 6 options in `PandaPal.lti_options` hash.
57
57
  ### Task Scheduling
58
58
  `PandaPal` includes an integration with `sidekiq-scheduler`. You can define tasks on an Organization class Stub like so:
59
59
  ```ruby
60
- # <your_app>/app/models/panda_pal/organization.rb
61
- require File.expand_path('../../app/models/panda_pal/organization.rb', PandaPal::Engine.called_from)
60
+ # <your_app>/app/models/organization_extension.rb
61
+ module OrganizationExtension
62
+ extend ActiveSupport::Concern
62
63
 
63
- module PandaPal
64
- class Organization
65
- # Will invoke CanvasSyncStarterWorker.perform_async() according to the cron schedule
66
- scheduled_task '0 15 05 * * *', :identifier, worker: CanvasSyncStarterWorker
64
+ # Will invoke CanvasSyncStarterWorker.perform_async() according to the cron schedule
65
+ scheduled_task '0 15 05 * * *', :identifier, worker: CanvasSyncStarterWorker
67
66
 
68
- # Will invoke the method 'organization_method' on the Organization
69
- scheduled_task '0 15 05 * * *', :organization_method_and_identifier
67
+ # Will invoke the method 'organization_method' on the Organization
68
+ scheduled_task '0 15 05 * * *', :organization_method_and_identifier
70
69
 
71
- # If you need to invoke the same method on multiple schedules
72
- scheduled_task '0 15 05 * * *', :identifier, worker: :organization_method
70
+ # If you need to invoke the same method on multiple schedules
71
+ scheduled_task '0 15 05 * * *', :identifier, worker: :organization_method
73
72
 
74
- # You can also use a block
75
- scheduled_task '0 15 05 * * *', :identifier do
76
- # Do Stuff
77
- end
73
+ # You can also use a block
74
+ scheduled_task '0 15 05 * * *', :identifier do
75
+ # Do Stuff
76
+ end
78
77
 
79
- # You can use a Proc (called in the context of the Organization) to determine the schedule
80
- scheduled_task -> { settings[:cron] }, :identifier
78
+ # You can use a Proc (called in the context of the Organization) to determine the schedule
79
+ scheduled_task -> { settings[:cron] }, :identifier
81
80
 
82
- # You can specify a timezone. If a TZ is not coded and settings[:timezone] is present, it will be appended automatically
83
- scheduled_task '0 15 05 * * * America/Denver', :identifier, worker: :organization_method
81
+ # You can specify a timezone. If a TZ is not coded and settings[:timezone] is present, it will be appended automatically
82
+ scheduled_task '0 15 05 * * * America/Denver', :identifier, worker: :organization_method
84
83
 
85
- # Setting settings[:task_schedules][:identifier] will override the code cron schedule. Setting it to false will disable the Task
86
- # :identifer values _must_ be unique, but can be nil, in which case they will be determined by where (lineno etc) scheduled_task is called
87
- end
84
+ # Setting settings[:task_schedules][:identifier] will override the code cron schedule. Setting it to false will disable the Task
85
+ # :identifer values _must_ be unique, but can be nil, in which case they will be determined by where (lineno etc) scheduled_task is called
88
86
  end
89
87
  ```
90
88
 
@@ -181,6 +179,18 @@ Rails.application.config.middleware.use 'Apartment::Elevators::Generic', lambda
181
179
  It is also possible to switch tenants by implementing `around_action :switch_tenant` in the controller.
182
180
  However, it should be noted that calling `switch_tenant` must be used in an `around_action` hook (or given a block), and is only intended to be used for LTI launches, and not subsequent requests.
183
181
 
182
+ ### Sharding
183
+ PandaPal 5.12 added support for multi-DB sharding. If you wish to use this, add a `shard` column to the `PandaPal::Organization` model. The implementation should be fairly transparent and should behave as normal PandaPal/Apartment - the only difference is that tenants can be created on different DB shards.
184
+
185
+ The list of available shards is computed from the Environment variables. On startup, the application looks for variables with the following formats:
186
+ - `SHARD_DB_XYZ_URL`
187
+ - `HEROKU_POSTGRESQL_XYZ_URL`
188
+ (where `XYZ` is the shard identifier)
189
+
190
+ When an `Organization` is created, it is assigned to a random shard (or if no shards were discovered, it is placed in the default database as normal). Alternatively, a specific shard can be specified: `PandaPal::Organization.new(shard: "shard_a")`. An `Organization` can be added to the primary shard with `shard: "default"`
191
+
192
+ The `PandaPal::Organization` record is still created in the `public` schema of the default database, as per usual.
193
+
184
194
  ### Rake tasks and jobs
185
195
  **Delayed Job Support has been removed. This allows each project to assess it's need to handle background jobs.**
186
196
 
data/Rakefile CHANGED
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
@@ -10,6 +10,7 @@ module PandaPal
10
10
 
11
11
  before_action :validate_launch!, only: [:resource_link_request]
12
12
  around_action :switch_tenant, only: [:resource_link_request]
13
+ before_action :enforce_environment!, only: [:resource_link_request]
13
14
 
14
15
  # Redirect to beta/test as necessary
15
16
  before_action :forward_to_env_and_region, only: [:login]
@@ -19,6 +20,8 @@ module PandaPal
19
20
 
20
21
  current_session_data[:lti_platform] = @current_lti_platform&.serialize
21
22
  current_session_data[:lti_oauth_nonce] = SecureRandom.uuid
23
+ current_session_data[:canvas_environment] = params['canvas_environment']
24
+ current_session_data[:canvas_region] = params['canvas_region']
22
25
  current_session.panda_pal_organization_id = -1
23
26
 
24
27
  @form_action = current_lti_platform.authentication_redirect_url
@@ -136,6 +139,17 @@ module PandaPal
136
139
  url
137
140
  end
138
141
 
142
+ def enforce_environment!
143
+ canvas_env = current_session_data[:canvas_environment]
144
+ return unless canvas_env.present?
145
+
146
+ org_canvas_url = current_organization.canvas_url
147
+
148
+ if (canvas_env == 'beta' || canvas_env == 'test') && org_canvas_url.present && !org_canvas_url.include?(".#{canvas_env}.")
149
+ render plain: "This tool is not properly configured for use in #{canvas_env}", status: 400
150
+ end
151
+ end
152
+
139
153
  private
140
154
 
141
155
  def auth_redirect_query
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
@@ -41,6 +41,9 @@ module PandaPal
41
41
  include OrganizationConcerns::OrganizationBuilder if $stdin&.tty?
42
42
  include OrganizationConcerns::TaskScheduling if defined?(Sidekiq.schedule)
43
43
 
44
+ include OrganizationConcerns::TenantHandling
45
+ include OrganizationConcerns::MultiDatabaseSharding if (column_names rescue []).include?('shard')
46
+
44
47
  attr_encrypted :settings, marshal: true, key: :encryption_key, marshaler: SettingsMarshaler
45
48
  before_save {|a| a.settings = a.settings} # this is a hacky work-around to a bug where attr_encrypted is not saving settings in place
46
49
 
@@ -56,9 +59,6 @@ module PandaPal
56
59
  validates :canvas_account_id, presence: true
57
60
  validates :salesforce_id, presence: true, uniqueness: true
58
61
 
59
- after_create :create_schema
60
- after_commit :destroy_schema, on: :destroy
61
-
62
62
  define_setting("lti", {
63
63
  type: 'Hash',
64
64
  required: false,
@@ -71,10 +71,6 @@ module PandaPal
71
71
  end
72
72
  end
73
73
 
74
- before_validation on: [:update] do
75
- errors.add(:name, 'should not be changed after creation') if name_changed?
76
- end
77
-
78
74
  def encryption_key
79
75
  # production environment might not have loaded secret_key_base yet.
80
76
  # In that case, just read it from env.
@@ -85,18 +81,6 @@ module PandaPal
85
81
  end
86
82
  end
87
83
 
88
- def switch_tenant(&block)
89
- if block_given?
90
- Apartment::Tenant.switch(name, &block)
91
- else
92
- Apartment::Tenant.switch!(name)
93
- end
94
- end
95
-
96
- def self.current
97
- find_by_name(Apartment::Tenant.current)
98
- end
99
-
100
84
  def create_api(logic, expiration: nil, uses: nil, host: nil)
101
85
  switch_tenant do
102
86
  logic = "current_organization.#{logic}" if logic.is_a?(Symbol)
@@ -109,16 +93,6 @@ module PandaPal
109
93
  end
110
94
  end
111
95
 
112
- def rename!(new_name)
113
- do_switch = Apartment::Tenant.current == name
114
- ActiveRecord::Base.connection.execute(
115
- "ALTER SCHEMA \"#{name}\" RENAME TO \"#{new_name}\";"
116
- )
117
- self.class.where(id: id).update_all(name: new_name)
118
- reload
119
- switch_tenant if do_switch
120
- end
121
-
122
96
  if !PandaPal.lti_options[:platform].present? || PandaPal.lti_options[:platform].is_a?(String)
123
97
  CONST_PLATFORM_TYPE = Platform.resolve_platform_class(nil) rescue nil
124
98
  else
@@ -192,16 +166,6 @@ module PandaPal
192
166
  CONST_PLATFORM_TYPE
193
167
  end
194
168
 
195
- private
196
-
197
- def create_schema
198
- Apartment::Tenant.create name
199
- end
200
-
201
- def destroy_schema
202
- Apartment::Tenant.drop name
203
- end
204
-
205
169
  public
206
170
 
207
171
  PandaPal.resolved_extensions_for(self).each do |ext|
@@ -0,0 +1,22 @@
1
+ module PandaPal
2
+ module OrganizationConcerns
3
+ module MultiDatabaseSharding
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ if column_names.include?('shard')
8
+ validates :shard, format: { with: /\A[a-z0-9_]+\z/i }, allow_blank: true
9
+
10
+ before_validation on: [:update] do
11
+ errors.add(:shard, 'should not be changed after creation') if shard_changed?
12
+ end
13
+ end
14
+ end
15
+
16
+ def tenant_name
17
+ return "#{shard}:#{name}" if shard.present?
18
+ super
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,54 @@
1
+ module PandaPal
2
+ module OrganizationConcerns
3
+ module TenantHandling
4
+ extend ActiveSupport::Concern
5
+
6
+ class_methods do
7
+ def current
8
+ find_by_name(Apartment::Tenant.current)
9
+ end
10
+ end
11
+
12
+ included do
13
+ after_create :create_schema
14
+ after_commit :destroy_schema, on: :destroy
15
+
16
+ before_validation on: [:update] do
17
+ errors.add(:name, 'should not be changed after creation') if name_changed?
18
+ end
19
+ end
20
+
21
+ def tenant_name
22
+ name
23
+ end
24
+
25
+ def switch_tenant(&block)
26
+ if block_given?
27
+ Apartment::Tenant.switch(tenant_name, &block)
28
+ else
29
+ Apartment::Tenant.switch!(tenant_name)
30
+ end
31
+ end
32
+
33
+ def rename!(new_name)
34
+ do_switch = Apartment::Tenant.current == name
35
+ ActiveRecord::Base.connection.execute(
36
+ "ALTER SCHEMA \"#{name}\" RENAME TO \"#{new_name}\";"
37
+ )
38
+ self.class.where(id: id).update_all(name: new_name)
39
+ reload
40
+ switch_tenant if do_switch
41
+ end
42
+
43
+ private
44
+
45
+ def create_schema
46
+ Apartment::Tenant.create tenant_name
47
+ end
48
+
49
+ def destroy_schema
50
+ Apartment::Tenant.drop tenant_name
51
+ end
52
+ end
53
+ end
54
+ end
File without changes
File without changes
File without changes
@@ -42,24 +42,39 @@ module PandaPal
42
42
  end
43
43
 
44
44
  def custom_lti_params
45
- # LT 1.3
46
- custom_params = launch_params["https://purl.imsglobal.org/spec/lti/claim/custom"]
47
- return custom_params if custom_params.present?
48
-
49
- # LTI 1.0/1.1
50
- custom_params = {}
51
- launch_params.each do |k, v|
52
- next unless k.start_with?("custom_")
53
- custom_params[k[7..-1]] = v
54
- end
45
+ @custom_lti_params ||= begin
46
+ # LT 1.3
47
+ custom_params = launch_params["https://purl.imsglobal.org/spec/lti/claim/custom"]
48
+ return custom_params if custom_params.present?
49
+
50
+ # LTI 1.0/1.1
51
+ custom_params = {}
52
+ launch_params.each do |k, v|
53
+ next unless k.start_with?("custom_")
54
+ custom_params[k[7..-1]] = v
55
+ end
55
56
 
56
- custom_params.with_indifferent_access
57
+ custom_params.with_indifferent_access
58
+ end
57
59
  end
58
60
 
59
- def get_lti_cust_param(key)
61
+ def get_lti_cust_param(key, default: :if_not_var)
60
62
  nkey = key.to_s.gsub(/^custom_/, '')
63
+ default_value = ->() { PandaPal.lti_custom_params[nkey] || PandaPal.lti_custom_params["custom_#{nkey}"] }
64
+
65
+ val = launch_params.dig("https://purl.imsglobal.org/spec/lti/claim/custom", nkey) || launch_params[nkey] || launch_params["custom_#{nkey}"]
66
+
67
+ if default == :if_not_var
68
+ if val.is_a?(String) && /\$[\.\w]+/.match?(val) && val == default_value[]
69
+ return nil
70
+ end
71
+ elsif default && !val.present?
72
+ return default_value[]
73
+ elsif !default && val == default_value[]
74
+ return nil
75
+ end
61
76
 
62
- launch_params.dig("https://purl.imsglobal.org/spec/lti/claim/custom", nkey) || launch_params[nkey] || launch_params["custom_#{nkey}"]
77
+ val
63
78
  end
64
79
 
65
80
  def canvas_role_labels
File without changes
File without changes
File without changes
File without changes
@@ -5,13 +5,212 @@ begin
5
5
  rescue LoadError
6
6
  end
7
7
 
8
+ require "apartment/adapters/postgresql_adapter"
9
+
10
+ module Apartment
11
+ SHARD_PREFIXES = ["SHARD_DB", "HEROKU_POSTGRESQL"]
12
+
13
+ def self.shard_configurations
14
+ $shard_configurations ||= begin
15
+ shard_to_env = {}
16
+
17
+ ENV.keys.each do |k|
18
+ m = /^(#{SHARD_PREFIXES.join("|")})_(\w+)_URL$/.match(k)
19
+ next unless m
20
+
21
+ url = ENV[k]
22
+ shard_to_env[m[2].downcase] = ActiveRecord::Base.configurations.resolve(url).configuration_hash
23
+ end
24
+
25
+ shard_to_env.freeze unless Rails.env.test?
26
+
27
+ shard_to_env
28
+ end
29
+ end
30
+
31
+ module Tenant
32
+ self.singleton_class.send(:alias_method, :original_postgresql_adapter, :postgresql_adapter)
33
+
34
+ def self.postgresql_adapter(config)
35
+ if Apartment.with_multi_server_setup
36
+ adapter = Adapters::PostgresMultiDBSchemaAdapter
37
+ adapter.new(config)
38
+ else
39
+ original_postgresql_adapter(config)
40
+ end
41
+ end
42
+
43
+ def self.split_tenant(tenant)
44
+ bits = tenant.split(":", 2)
45
+ bits.unshift(nil) if bits.length == 1
46
+ bits[0] = "default" if bits[0].nil? || bits[0].empty?
47
+ bits
48
+ end
49
+ end
50
+
51
+ module Adapters
52
+ class PostgresMultiDBSchemaAdapter < Apartment::Adapters::PostgresqlSchemaAdapter
53
+ def initialize(*args, **kwargs)
54
+ super
55
+ @excluded_model_set = Set.new(Apartment.excluded_models)
56
+ end
57
+
58
+ def process_excluded_model(excluded_model)
59
+ @excluded_model_set << excluded_model
60
+ super
61
+ end
62
+
63
+ def is_excluded_model?(model)
64
+ @excluded_model_set.include?(model.to_s)
65
+ end
66
+
67
+ def db_connection_config(tenant)
68
+ shard, schema = Tenant.split_tenant(tenant)
69
+ if shard == "default"
70
+ @config
71
+ else
72
+ Apartment.shard_configurations[shard.downcase]
73
+ end
74
+ end
75
+
76
+ def drop_command(conn, tenant)
77
+ shard, schema = Tenant.split_tenant(tenant)
78
+ conn.execute(%(DROP SCHEMA "#{schema}" CASCADE))
79
+ end
80
+
81
+ def tenant_exists?(tenant)
82
+ return true unless Apartment.tenant_presence_check
83
+ shard, schema = Tenant.split_tenant(tenant)
84
+
85
+ Apartment.connection.schema_exists?(schema)
86
+ end
87
+
88
+ def create_tenant_command(conn, tenant)
89
+ shard, schema = Tenant.split_tenant(tenant)
90
+ # NOTE: This was causing some tests to fail because of the database strategy for rspec
91
+ if conn.open_transactions.positive?
92
+ conn.execute(%(CREATE SCHEMA "#{schema}")).inspect
93
+ else
94
+ schema = %(BEGIN;
95
+ CREATE SCHEMA "#{schema}";
96
+ COMMIT;)
97
+
98
+ conn.execute(schema)
99
+ end
100
+ rescue *rescuable_exceptions => e
101
+ rollback_transaction(conn)
102
+ raise e
103
+ end
104
+
105
+ def connect_to_new(tenant = nil)
106
+ return reset if tenant.nil?
107
+
108
+ current_tenant = @current
109
+ tenants_array = tenant.is_a?(Array) ? tenant.map(&:to_s) : tenant.to_s
110
+ tenant_schemas = map_to_schemas(Array(tenants_array))
111
+ query_cache_enabled = ActiveRecord::Base.connection.query_cache_enabled
112
+
113
+ @current = tenants_array
114
+
115
+ raise ActiveRecord::StatementInvalid, "PandaPal/Apartment Mutli-DB support does not currently support DB roles" if ActiveRecord::Base.current_role != ActiveRecord::Base.default_role
116
+
117
+ unless ActiveRecord::Base.connected?
118
+ Apartment.establish_connection multi_tenantify(tenant, false)
119
+ Apartment.connection.verify!
120
+ end
121
+
122
+ Apartment.connection.enable_query_cache! if query_cache_enabled
123
+
124
+ raise ActiveRecord::StatementInvalid, "Could not find schema for tenant #{tenant} (#{tenant_schemas.inspect})" unless schema_exists?(tenant_schemas)
125
+
126
+ Apartment.connection.schema_search_path = full_search_path
127
+ rescue *rescuable_exceptions => e
128
+ @current = current_tenant
129
+ raise_schema_connect_to_new(tenant, e)
130
+ end
131
+
132
+ protected
133
+
134
+ def persistent_schemas
135
+ map_to_schemas(super)
136
+ end
137
+
138
+ def map_to_schemas(tenants)
139
+ all_shard = nil
140
+ tenants.map do |schema|
141
+ shard, schema = Tenant.split_tenant(schema)
142
+ all_shard ||= shard
143
+ raise "Cannot mix shards in persistent_schemas" if shard != all_shard
144
+ schema
145
+ end
146
+ end
147
+ end
148
+ end
149
+
150
+ module CorePatches
151
+ module ActiveRecord
152
+ module FutureResult
153
+ # called in the original thread that knows the tenant
154
+ def initialize(_pool, *_args, **_kwargs)
155
+ @tenant = Apartment::Tenant.current
156
+ super
157
+ end
158
+
159
+ # called in the new thread with a connection that needs switching
160
+ def exec_query(_conn, *_args, **_kwargs)
161
+ Apartment::Tenant.switch!(@tenant) unless Apartment::Tenant.current == @tenant
162
+ super
163
+ end
164
+ end
165
+
166
+ module Base
167
+ extend ActiveSupport::Concern
168
+
169
+ included do
170
+ self.singleton_class.send(:alias_method, :pre_apartment_current_shard, :current_shard)
171
+
172
+ def self.current_shard
173
+ # This implementation is definitely a hack, but it should be fairly compatible. If you need to leverage
174
+ # Rails' sharding natively - you should just need to add models to the excluded_models list in Apartment
175
+ # to effectively disable this patch for that model.
176
+ if (adapter = Thread.current[:apartment_adapter]) && adapter.is_a?(Apartment::Adapters::PostgresMultiDBSchemaAdapter) && !adapter.is_excluded_model?(self)
177
+ shard, schema = Apartment::Tenant.split_tenant(adapter.current)
178
+ return "apt:#{shard}" unless shard == "default"
179
+ end
180
+
181
+ pre_apartment_current_shard
182
+ end
183
+ end
184
+ end
185
+ end
186
+ end
187
+
188
+ # Fix Apartment's lack of support for load_async
189
+ ::ActiveRecord::FutureResult.prepend CorePatches::ActiveRecord::FutureResult
190
+
191
+ # Hack ActiveRecord shards/connection_pooling to support our multi-DB approach
192
+ ::ActiveRecord::Base.include CorePatches::ActiveRecord::Base
193
+ end
194
+
8
195
  Apartment.configure do |config|
9
196
  config.excluded_models ||= []
10
197
  config.excluded_models |= ['PandaPal::Organization', 'PandaPal::Session']
11
198
 
12
- config.tenant_names = lambda {
13
- PandaPal::Organization.pluck(:name)
14
- }
199
+ config.with_multi_server_setup = true unless Rails.env.test?
200
+
201
+ config.tenant_names = lambda do
202
+ if PandaPal::Organization < PandaPal::OrganizationConcerns::MultiDatabaseSharding
203
+ base_config = Apartment.connection_config
204
+ shard_configurations = Apartment.shard_configurations
205
+
206
+ PandaPal::Organization.all.to_a.each_with_object({}) do |org, hash|
207
+ shard = org.shard || "default"
208
+ hash[org.tenant_name] = shard == "default" ? base_config : shard_configurations[shard.downcase]
209
+ end
210
+ else
211
+ PandaPal::Organization.pluck(:name)
212
+ end
213
+ end
15
214
  end
16
215
 
17
216
  Rails.application.config.middleware.use Apartment::Elevators::Generic, lambda { |request|
data/config/routes.rb CHANGED
File without changes
File without changes
@@ -7,8 +7,6 @@ require_relative './helpers/console_helpers'
7
7
 
8
8
  module PandaPal
9
9
  class Engine < ::Rails::Engine
10
- config.autoload_once_paths += Dir["#{config.root}/lib/**/"]
11
-
12
10
  isolate_namespace PandaPal
13
11
 
14
12
  config.generators do |g|
File without changes
File without changes
File without changes