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.
- checksums.yaml +4 -4
- data/MIT-LICENSE +0 -0
- data/README.md +31 -21
- data/Rakefile +0 -0
- data/app/assets/config/panda_pal_manifest.js +0 -0
- data/app/assets/javascripts/panda_pal/application.js +0 -0
- data/app/assets/javascripts/panda_pal/lti.js +0 -0
- data/app/assets/stylesheets/panda_pal/application.css +0 -0
- data/app/assets/stylesheets/panda_pal/lti.css +0 -0
- data/app/controllers/panda_pal/api_call_controller.rb +0 -0
- data/app/controllers/panda_pal/application_controller.rb +0 -0
- data/app/controllers/panda_pal/lti_controller.rb +0 -0
- data/app/controllers/panda_pal/lti_v1_p0_controller.rb +0 -0
- data/app/controllers/panda_pal/lti_v1_p3_controller.rb +14 -0
- data/app/helpers/panda_pal/application_helper.rb +0 -0
- data/app/jobs/panda_pal/jobs/grade_passback_job.rb +0 -0
- data/app/lib/lti_xml/base_platform.rb +0 -0
- data/app/lib/lti_xml/bridge_platform.rb +0 -0
- data/app/lib/lti_xml/canvas_platform.rb +0 -0
- data/app/lib/panda_pal/launch_url_helpers.rb +0 -0
- data/app/lib/panda_pal/lti_jwt_validator.rb +0 -0
- data/app/models/panda_pal/api_call.rb +0 -0
- data/app/models/panda_pal/organization.rb +3 -39
- data/app/models/panda_pal/organization_concerns/multi_database_sharding.rb +22 -0
- data/app/models/panda_pal/organization_concerns/organization_builder.rb +0 -0
- data/app/models/panda_pal/organization_concerns/settings_validation.rb +0 -0
- data/app/models/panda_pal/organization_concerns/task_scheduling.rb +0 -0
- data/app/models/panda_pal/organization_concerns/tenant_handling.rb +54 -0
- data/app/models/panda_pal/panda_pal_record.rb +0 -0
- data/app/models/panda_pal/platform/canvas.rb +0 -0
- data/app/models/panda_pal/platform.rb +0 -0
- data/app/models/panda_pal/session.rb +28 -13
- data/app/views/layouts/panda_pal/application.html.erb +0 -0
- data/app/views/panda_pal/lti/launch.html.erb +0 -0
- data/app/views/panda_pal/lti_v1_p3/login.html.erb +0 -0
- data/app/views/panda_pal/partials/_auto_submit_form.html.erb +0 -0
- data/config/dev_lti_key.key +0 -0
- data/config/initializers/apartment.rb +202 -3
- data/config/routes.rb +0 -0
- data/db/migrate/20160412205931_create_panda_pal_organizations.rb +0 -0
- data/db/migrate/20160413135653_create_panda_pal_sessions.rb +0 -0
- data/db/migrate/20160425130344_add_panda_pal_organization_to_session.rb +0 -0
- data/db/migrate/20170106165533_add_salesforce_id_to_organizations.rb +0 -0
- data/db/migrate/20171205183457_encrypt_organization_settings.rb +0 -0
- data/db/migrate/20171205194657_remove_old_organization_settings.rb +0 -0
- data/db/migrate/20220721095653_create_panda_pal_api_calls.rb +0 -0
- data/lib/panda_pal/concerns/ability_helper.rb +0 -0
- data/lib/panda_pal/engine.rb +0 -2
- data/lib/panda_pal/helpers/console_helpers.rb +0 -0
- data/lib/panda_pal/helpers/controller_helper.rb +0 -0
- data/lib/panda_pal/helpers/misc_helper.rb +0 -0
- data/lib/panda_pal/helpers/route_helper.rb +0 -0
- data/lib/panda_pal/helpers/secure_headers.rb +0 -0
- data/lib/panda_pal/helpers/session_replacement.rb +0 -0
- data/lib/panda_pal/helpers.rb +0 -0
- data/lib/panda_pal/plugins.rb +0 -0
- data/lib/panda_pal/spec_helper.rb +82 -0
- data/lib/panda_pal/version.rb +1 -1
- data/lib/panda_pal.rb +1 -1
- data/lib/tasks/panda_pal_tasks.rake +0 -0
- data/panda_pal.gemspec +3 -3
- data/spec/controllers/panda_pal/api_call_controller_spec.rb +0 -0
- data/spec/core/apartment_multidb_spec.rb +48 -0
- data/spec/dummy/README.rdoc +0 -0
- data/spec/dummy/Rakefile +0 -0
- data/spec/dummy/app/assets/javascripts/application.js +0 -0
- data/spec/dummy/app/assets/stylesheets/application.css +0 -0
- data/spec/dummy/app/controllers/application_controller.rb +0 -0
- data/spec/dummy/app/helpers/application_helper.rb +0 -0
- data/spec/dummy/app/views/layouts/application.html.erb +0 -0
- data/spec/dummy/config/application.rb +0 -0
- data/spec/dummy/config/boot.rb +0 -0
- data/spec/dummy/config/database.yml +6 -2
- data/spec/dummy/config/environment.rb +0 -0
- data/spec/dummy/config/environments/development.rb +0 -0
- data/spec/dummy/config/environments/production.rb +0 -0
- data/spec/dummy/config/environments/test.rb +5 -0
- data/spec/dummy/config/initializers/backtrace_silencers.rb +0 -0
- data/spec/dummy/config/initializers/cookies_serializer.rb +0 -0
- data/spec/dummy/config/initializers/filter_parameter_logging.rb +0 -0
- data/spec/dummy/config/initializers/inflections.rb +0 -0
- data/spec/dummy/config/initializers/mime_types.rb +0 -0
- data/spec/dummy/config/initializers/session_store.rb +0 -0
- data/spec/dummy/config/initializers/wrap_parameters.rb +0 -0
- data/spec/dummy/config/locales/en.yml +0 -0
- data/spec/dummy/config/routes.rb +0 -0
- data/spec/dummy/config/secrets.yml +0 -0
- data/spec/dummy/config.ru +0 -0
- data/spec/dummy/db/schema.rb +0 -0
- data/spec/dummy/db/test2_schema.rb +18 -0
- data/spec/dummy/public/404.html +0 -0
- data/spec/dummy/public/422.html +0 -0
- data/spec/dummy/public/500.html +0 -0
- data/spec/dummy/public/favicon.ico +0 -0
- data/spec/factories/panda_pal_organizations.rb +0 -0
- data/spec/factories/panda_pal_sessions.rb +0 -0
- data/spec/models/panda_pal/api_call_spec.rb +1 -1
- data/spec/models/panda_pal/organization/settings_validation_spec.rb +4 -0
- data/spec/models/panda_pal/organization_spec.rb +0 -0
- data/spec/models/panda_pal/session_spec.rb +0 -0
- data/spec/rails_helper.rb +4 -0
- data/spec/spec_helper.rb +0 -1
- metadata +57 -50
- /data/spec/dummy/{bin → app/bin}/bundle +0 -0
- /data/spec/dummy/{bin → app/bin}/rails +0 -0
- /data/spec/dummy/{bin → app/bin}/rake +0 -0
- /data/spec/dummy/{bin → app/bin}/setup +0 -0
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ab899936461dc59208ae6afef28264feda7a75927798e158ca1ee8381b2ea463
|
4
|
+
data.tar.gz: 20424ac6bef9bede8af9e58e8a1393267b0966f96cf56dd8ae8ed0e5b486420a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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/
|
61
|
-
|
60
|
+
# <your_app>/app/models/organization_extension.rb
|
61
|
+
module OrganizationExtension
|
62
|
+
extend ActiveSupport::Concern
|
62
63
|
|
63
|
-
|
64
|
-
|
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
|
-
|
69
|
-
|
67
|
+
# Will invoke the method 'organization_method' on the Organization
|
68
|
+
scheduled_task '0 15 05 * * *', :organization_method_and_identifier
|
70
69
|
|
71
|
-
|
72
|
-
|
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
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
73
|
+
# You can also use a block
|
74
|
+
scheduled_task '0 15 05 * * *', :identifier do
|
75
|
+
# Do Stuff
|
76
|
+
end
|
78
77
|
|
79
|
-
|
80
|
-
|
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
|
-
|
83
|
-
|
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
|
-
|
86
|
-
|
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
|
File without changes
|
File without changes
|
File without changes
|
@@ -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
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
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
|
-
|
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
|
-
|
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
|
data/config/dev_lti_key.key
CHANGED
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.
|
13
|
-
|
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
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
data/lib/panda_pal/engine.rb
CHANGED
File without changes
|
File without changes
|
File without changes
|