pg_rls 0.0.2.6.11 → 0.1.0

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.
@@ -5,7 +5,7 @@ module PgRls
5
5
  # Up Schema Statements
6
6
  module UpStatements
7
7
  def create_rls_user(name: PgRls.username, password: PgRls.password, schema: 'public')
8
- PgRls.execute <<-SQL
8
+ ActiveRecord::Migration.execute <<-SQL
9
9
  DO
10
10
  $do$
11
11
  BEGIN
@@ -35,7 +35,7 @@ module PgRls
35
35
  end
36
36
 
37
37
  def create_rls_blocking_function
38
- ActiveRecord::Migration.execute <<-SQL
38
+ ActiveRecord::Migration.execute <<-SQL.squish
39
39
  CREATE OR REPLACE FUNCTION id_safe_guard ()
40
40
  RETURNS TRIGGER LANGUAGE plpgsql AS $$
41
41
  BEGIN
@@ -45,7 +45,7 @@ module PgRls
45
45
  end
46
46
 
47
47
  def create_rls_setter_function
48
- ActiveRecord::Migration.execute <<-SQL
48
+ ActiveRecord::Migration.execute <<-SQL.squish
49
49
  CREATE OR REPLACE FUNCTION tenant_id_setter ()
50
50
  RETURNS TRIGGER LANGUAGE plpgsql AS $$
51
51
  BEGIN
@@ -56,7 +56,7 @@ module PgRls
56
56
  end
57
57
 
58
58
  def append_blocking_function(table_name)
59
- ActiveRecord::Migration.execute <<-SQL
59
+ ActiveRecord::Migration.execute <<-SQL.squish
60
60
  CREATE TRIGGER id_safe_guard
61
61
  BEFORE UPDATE OF id ON #{table_name}
62
62
  FOR EACH ROW EXECUTE PROCEDURE id_safe_guard();
@@ -64,7 +64,7 @@ module PgRls
64
64
  end
65
65
 
66
66
  def append_trigger_function(table_name)
67
- ActiveRecord::Migration.execute <<-SQL
67
+ ActiveRecord::Migration.execute <<-SQL.squish
68
68
  CREATE TRIGGER tenant_id_setter
69
69
  BEFORE INSERT OR UPDATE ON #{table_name}
70
70
  FOR EACH ROW EXECUTE PROCEDURE tenant_id_setter();
@@ -72,7 +72,7 @@ module PgRls
72
72
  end
73
73
 
74
74
  def add_rls_column_to_tenant_table(table_name)
75
- ActiveRecord::Migration.execute <<-SQL
75
+ ActiveRecord::Migration.execute <<-SQL.squish
76
76
  ALTER TABLE #{table_name}
77
77
  ADD COLUMN IF NOT EXISTS
78
78
  tenant_id uuid UNIQUE DEFAULT gen_random_uuid();
@@ -80,7 +80,7 @@ module PgRls
80
80
  end
81
81
 
82
82
  def add_rls_column(table_name)
83
- ActiveRecord::Migration.execute <<-SQL
83
+ ActiveRecord::Migration.execute <<-SQL.squish
84
84
  ALTER TABLE #{table_name}
85
85
  ADD COLUMN IF NOT EXISTS tenant_id uuid,
86
86
  ADD CONSTRAINT fk_#{PgRls.table_name}
@@ -91,7 +91,7 @@ module PgRls
91
91
  end
92
92
 
93
93
  def create_rls_policy(table_name, user = PgRls.username)
94
- ActiveRecord::Migration.execute <<-SQL
94
+ ActiveRecord::Migration.execute <<-SQL.squish
95
95
  ALTER TABLE #{table_name} ENABLE ROW LEVEL SECURITY;
96
96
  CREATE POLICY #{table_name}_#{user}
97
97
  ON #{table_name}
data/lib/pg_rls/tenant.rb CHANGED
@@ -25,16 +25,10 @@ module PgRls
25
25
  raise e
26
26
  end
27
27
 
28
- def find_each(&)
29
- PgRls.main_model.find_each do |tenant|
30
- with_tenant!(tenant, &)
31
- end
32
- end
33
-
34
- def with_tenant!(resource, &block)
28
+ def with_tenant!(resource)
35
29
  tenant = switch_tenant!(resource)
36
30
 
37
- block.call(tenant) if block_given?
31
+ yield(tenant) if block_given?
38
32
  ensure
39
33
  reset_rls! unless PgRls.test_inline_tenant == true
40
34
  end
@@ -45,47 +39,50 @@ module PgRls
45
39
  nil
46
40
  end
47
41
 
48
- def fetch!
42
+ def tenant!
49
43
  @tenant ||= PgRls.main_model.find_by!(
50
44
  tenant_id: PgRls.connection_class.connection.execute(
51
45
  "SELECT current_setting('rls.tenant_id')"
52
46
  ).getvalue(0, 0)
53
47
  )
54
48
  end
55
-
56
- def find_main_model
57
- PgRls.main_model.ignored_columns = []
58
- PgRls.main_model.find_by!(
59
- tenant_id: PgRls.connection_class.connection.execute(
60
- "SELECT current_setting('rls.tenant_id')"
61
- ).getvalue(0, 0)
62
- )
63
- end
49
+ alias fetch! tenant!
64
50
 
65
51
  def reset_rls!
52
+ return if @tenant.blank?
53
+
66
54
  @tenant = nil
67
- PgRls.connection_class.connection.execute('RESET rls.tenant_id')
55
+ PgRls.execute_rls_in_shards do |connection_class|
56
+ connection_class.transaction do
57
+ connection_class.connection.execute('RESET rls.tenant_id')
58
+ end
59
+ end
60
+
61
+ nil
68
62
  end
69
63
 
70
64
  private
71
65
 
72
66
  def switch_tenant!(resource)
67
+ # rubocop: disable Rails/IgnoredColumnsAssignment
73
68
  PgRls.main_model.ignored_columns = []
69
+ # rubocop: enable Rails/IgnoredColumnsAssignment
74
70
 
75
- connection_adapter = PgRls.connection_class
76
71
  find_tenant(resource)
77
72
 
78
- raise PgRls::Errors::TenantNotFound if tenant.blank?
79
-
80
- connection_adapter.connection.transaction do
81
- connection_adapter.connection.execute(format('SET rls.tenant_id = %s',
82
- connection_adapter.connection.quote(tenant.tenant_id)))
73
+ PgRls.execute_rls_in_shards do |connection_class|
74
+ connection_class.transaction do
75
+ connection_class.connection.execute(format('SET rls.tenant_id = %s',
76
+ connection_class.connection.quote(tenant.tenant_id)))
77
+ end
83
78
  end
84
79
 
85
80
  tenant
86
81
  end
87
82
 
88
83
  def find_tenant(resource)
84
+ raise PgRls::Errors::AdminUsername if admin_username?
85
+
89
86
  reset_rls!
90
87
 
91
88
  PgRls.search_methods.each do |method|
@@ -95,7 +92,11 @@ module PgRls
95
92
  @tenant = find_tenant_by_method(resource, method)
96
93
  end
97
94
 
98
- raise PgRls::Errors::TenantNotFound if tenant.nil?
95
+ raise PgRls::Errors::TenantNotFound if tenant.blank?
96
+ end
97
+
98
+ def admin_username?
99
+ PgRls.username != PgRls.current_db_username
99
100
  end
100
101
 
101
102
  def find_tenant_by_method(resource, method)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module PgRls
4
- VERSION = '0.0.2.6.11'
4
+ VERSION = '0.1.0'
5
5
  end
data/lib/pg_rls.rb CHANGED
@@ -5,13 +5,11 @@ require 'forwardable'
5
5
  require_relative 'pg_rls/version'
6
6
  require_relative 'pg_rls/database/prepared'
7
7
  require_relative 'pg_rls/schema/statements'
8
- require_relative 'pg_rls/schema/solo/statements'
9
- require_relative 'pg_rls/solo/tenant'
8
+ require_relative 'pg_rls/database/configurations'
10
9
  require_relative 'pg_rls/tenant'
11
- require_relative 'pg_rls/secure_connection'
12
10
  require_relative 'pg_rls/multi_tenancy'
13
11
  require_relative 'pg_rls/railtie' if defined?(Rails)
14
- require_relative 'pg_rls/errors/tenant_not_found'
12
+ require_relative 'pg_rls/errors/index'
15
13
 
16
14
  # PostgreSQL Row Level Security
17
15
  module PgRls
@@ -21,13 +19,13 @@ module PgRls
21
19
  class << self
22
20
  extend Forwardable
23
21
 
24
- WRITER_METHODS = %i[table_name class_name search_methods establish_default_connection].freeze
22
+ WRITER_METHODS = %i[table_name class_name search_methods].freeze
25
23
  READER_METHODS = %i[
26
- connection_class database_configuration execute table_name class_name search_methods establish_default_connection
24
+ connection_class execute table_name class_name search_methods
27
25
  ].freeze
28
26
  DELEGATORS_METHODS = %i[
29
- connection_class database_configuration execute table_name search_methods
30
- class_name all_tenants main_model establish_default_connection
27
+ connection_class execute table_name search_methods
28
+ class_name main_model
31
29
  ].freeze
32
30
 
33
31
  attr_writer(*WRITER_METHODS)
@@ -36,100 +34,104 @@ module PgRls
36
34
  def_delegators(*DELEGATORS_METHODS)
37
35
 
38
36
  def setup
37
+ ActiveRecord::ConnectionAdapters::AbstractAdapter.include PgRls::Schema::Statements
38
+ ActiveRecord::Base.ignored_columns += %w[tenant_id]
39
+
39
40
  yield self
40
41
  end
41
42
 
42
- def solo_mode?
43
- solo_mode
43
+ def connection_class
44
+ @connection_class ||= ActiveRecord::Base
44
45
  end
45
46
 
46
- def database_connection_file
47
- file = File.read(Rails.root.join('config/database.yml'))
48
-
49
- YAML.safe_load(ERB.new(file).result, aliases: true)
47
+ def rake_tasks?
48
+ Rake.application.top_level_tasks.present? || ARGV.any? { |arg| arg =~ /rake|dsl/ }
49
+ rescue NoMethodError
50
+ false
50
51
  end
51
52
 
52
- def connection_class
53
- @connection_class ||= ActiveRecord::Base
54
- end
53
+ def admin_tasks_execute
54
+ raise PgRls::Errors::RakeOnlyError unless rake_tasks?
55
55
 
56
- def establish_new_connection
57
- ActiveRecord::Base.connection.disconnect! if ActiveRecord::Base.connection_pool.connected?
56
+ self.as_db_admin = true
58
57
 
59
- connection_class.establish_connection(**database_configuration)
58
+ yield
59
+ ensure
60
+ self.as_db_admin = false
60
61
  end
61
62
 
62
- def admin_execute(query = nil)
63
+ def admin_execute(query = nil, &)
64
+ current_tenant = PgRls::Tenant.fetch
65
+
63
66
  self.as_db_admin = true
64
- establish_new_connection
65
- return yield if block_given?
67
+ establish_new_connection!
68
+
69
+ return ensure_block_execution(&) if block_given?
66
70
 
67
71
  execute(query)
68
72
  ensure
69
73
  self.as_db_admin = false
70
- establish_new_connection
71
- end
72
-
73
- def establish_default_connection=(value)
74
- ENV['AS_DB_ADMIN'] = value.to_s
75
- @default_connection = value
74
+ establish_new_connection!
75
+ PgRls::Tenant.switch(current_tenant) if current_tenant.present?
76
76
  end
77
77
 
78
- def default_connection?
79
- as_db_admin
78
+ def establish_new_connection!
79
+ execute_rls_in_shards do |connection_class, pool|
80
+ connection_class.remove_connection
81
+ connection_class.establish_connection(pool.db_config)
82
+ end
80
83
  end
81
84
 
82
85
  def main_model
83
86
  class_name.to_s.camelize.constantize
84
87
  end
85
88
 
86
- def all_tenants
87
- main_model.all.each do |tenant|
89
+ def on_each_tenant(&)
90
+ result = []
91
+ main_model.find_each do |tenant|
88
92
  allowed_search_fields = search_methods.map(&:to_s).intersection(main_model.column_names)
89
93
  Tenant.switch tenant.send(allowed_search_fields.first)
90
94
 
91
- yield(tenant) if block_given?
95
+ result << { tenant:, result: ensure_block_execution(tenant, &) }
92
96
  end
93
- end
94
97
 
95
- def current_connection_username
96
- connection_class.connection_db_config.configuration_hash[:username]
97
- end
98
+ PgRls::Tenant.reset_rls!
98
99
 
99
- def execute(query)
100
- ActiveRecord::Migration.execute(query)
100
+ result
101
101
  end
102
102
 
103
- def database_default_configuration
104
- connection_class.connection.pool.db_config.configuration_hash
105
- rescue ActiveRecord::NoDatabaseError
106
- connection_class.connection_db_config.configuration_hash
107
- end
103
+ def execute_rls_in_shards
104
+ connection_pool_list = PgRls.connection_class.connection_handler.connection_pool_list
105
+ result = []
108
106
 
109
- def database_admin_configuration
110
- environment_db_configuration = database_connection_file[Rails.env]
107
+ connection_pool_list.each do |pool|
108
+ pool.connection.transaction do
109
+ Rails.logger.info("Executing in #{pool.connection.connection_class}")
111
110
 
112
- return environment_db_configuration if environment_db_configuration['username'].present?
111
+ result << yield(pool.connection.connection_class, pool)
112
+ end
113
+ end
113
114
 
114
- environment_db_configuration.first.last
115
+ result
115
116
  end
116
117
 
117
- def database_configuration
118
- return database_admin_configuration if default_connection?
118
+ def current_db_username
119
+ ActiveRecord::Base.connection_db_config.configuration_hash[:username]
120
+ end
119
121
 
120
- current_configuration = database_default_configuration.deep_dup
121
- current_configuration.tap do |config|
122
- config[:username] = PgRls.username
123
- config[:password] = PgRls.password
124
- end
122
+ def as_db_admin?
123
+ @as_db_admin || false
124
+ end
125
+
126
+ private
125
127
 
126
- current_configuration.freeze
128
+ attr_writer :as_db_admin
129
+
130
+ def ensure_block_execution(*, **)
131
+ yield(*, **).presence
127
132
  end
128
133
  end
129
134
 
130
- mattr_accessor :as_db_admin
131
- @@as_db_admin = false
132
-
133
135
  mattr_accessor :table_name
134
136
  @@table_name = 'companies'
135
137
 
@@ -142,18 +144,9 @@ module PgRls
142
144
  mattr_accessor :password
143
145
  @@password = 'password'
144
146
 
145
- mattr_accessor :solo_mode
146
- @@solo_mode = false
147
-
148
147
  mattr_accessor :test_inline_tenant
149
148
  @@test_inline_tenant = false
150
149
 
151
- mattr_accessor :session_key
152
- @@session_key = '_hub_sessions'
153
-
154
- mattr_accessor :session_prefix
155
- @@session_prefix = '_session_id:2::'
156
-
157
150
  mattr_accessor :search_methods
158
151
  @@search_methods = %i[subdomain id tenant_id]
159
152
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pg_rls
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.2.6.11
4
+ version: 0.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Daniel Laloush
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2023-02-20 00:00:00.000000000 Z
11
+ date: 2023-10-14 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -61,8 +61,12 @@ files:
61
61
  - lib/generators/templates/pg_rls.rb.tt
62
62
  - lib/pg_rls.rb
63
63
  - lib/pg_rls/Rakefile
64
+ - lib/pg_rls/database/configurations.rb
64
65
  - lib/pg_rls/database/prepared.rb
65
66
  - lib/pg_rls/database/tasks/admin_database.rake
67
+ - lib/pg_rls/errors/admin_username.rb
68
+ - lib/pg_rls/errors/index.rb
69
+ - lib/pg_rls/errors/rake_only_error.rb
66
70
  - lib/pg_rls/errors/tenant_not_found.rb
67
71
  - lib/pg_rls/middleware.rb
68
72
  - lib/pg_rls/middleware/set_reset_connection.rb
@@ -72,12 +76,8 @@ files:
72
76
  - lib/pg_rls/multi_tenancy.rb
73
77
  - lib/pg_rls/railtie.rb
74
78
  - lib/pg_rls/schema/down_statements.rb
75
- - lib/pg_rls/schema/solo/statements.rb
76
- - lib/pg_rls/schema/solo/up_statements.rb
77
79
  - lib/pg_rls/schema/statements.rb
78
80
  - lib/pg_rls/schema/up_statements.rb
79
- - lib/pg_rls/secure_connection.rb
80
- - lib/pg_rls/solo/tenant.rb
81
81
  - lib/pg_rls/tenant.rb
82
82
  - lib/pg_rls/version.rb
83
83
  homepage: https://github.com/Dandush03/pg_rls
@@ -93,14 +93,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
93
93
  requirements:
94
94
  - - "~>"
95
95
  - !ruby/object:Gem::Version
96
- version: '3.1'
96
+ version: '3.2'
97
97
  required_rubygems_version: !ruby/object:Gem::Requirement
98
98
  requirements:
99
99
  - - ">="
100
100
  - !ruby/object:Gem::Version
101
101
  version: '0'
102
102
  requirements: []
103
- rubygems_version: 3.3.26
103
+ rubygems_version: 3.4.19
104
104
  signing_key:
105
105
  specification_version: 4
106
106
  summary: Write a short summary, because RubyGems requires one.
@@ -1,24 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative '../statements'
4
- require_relative './up_statements'
5
-
6
- module PgRls
7
- module Schema
8
- module Solo
9
- # Schema Solo Statements
10
- module Statements
11
- include PgRls::Schema::Statements
12
- include PgRls::Schema::Solo::UpStatements
13
-
14
- def create_rls_table(table_name, **options, &)
15
- setup_rls_tenant_table
16
- create_table(table_name, **options, &)
17
- add_rls_column(table_name)
18
- create_rls_policy(table_name)
19
- append_trigger_function(table_name)
20
- end
21
- end
22
- end
23
- end
24
- end
@@ -1,106 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module PgRls
4
- module Schema
5
- module Solo
6
- # Up Schema Solo Statements
7
- module UpStatements
8
- def setup_rls_tenant_table
9
- ActiveRecord::Migration.execute <<-SQL
10
- DO
11
- $do$
12
- BEGIN
13
- IF NOT EXISTS (
14
- SELECT FROM pg_tables
15
- WHERE schemaname = 'public' AND tablename = '#{PgRls.table_name}') THEN
16
- #{create_rls_user}
17
- #{create_rls_setter_function}
18
- #{create_rls_blocking_function}
19
- #{create_rls_solo_tenant_table}
20
- #{append_blocking_function}
21
- END IF;
22
- END;
23
- $do$;
24
- SQL
25
- end
26
-
27
- def create_rls_user(name: PgRls.username, password: PgRls.password, schema: 'public')
28
- <<~SQL
29
- -- Grant Role Permissions
30
- BEGIN
31
- IF NOT EXISTS (
32
- SELECT FROM pg_catalog.pg_roles -- SELECT list can be empty for this
33
- WHERE rolname = '#{name}') THEN
34
-
35
- CREATE USER #{name} WITH PASSWORD '#{password}';
36
- END IF;
37
- GRANT ALL PRIVILEGES ON TABLE schema_migrations TO #{name};
38
- GRANT USAGE ON SCHEMA #{schema} TO #{name};
39
- ALTER DEFAULT PRIVILEGES IN SCHEMA #{schema}
40
- GRANT USAGE, SELECT
41
- ON SEQUENCES TO #{name};
42
- ALTER DEFAULT PRIVILEGES IN SCHEMA #{schema}
43
- GRANT SELECT, INSERT, UPDATE, DELETE
44
- ON TABLES TO #{name};
45
- GRANT SELECT, INSERT, UPDATE, DELETE
46
- ON ALL TABLES IN SCHEMA #{schema}
47
- TO #{name};
48
- GRANT USAGE, SELECT
49
- ON ALL SEQUENCES IN SCHEMA #{schema}
50
- TO #{name};
51
- END;
52
- SQL
53
- end
54
-
55
- def create_rls_setter_function
56
- <<~SQL
57
- -- Create RLS Setter Function
58
- CREATE OR REPLACE FUNCTION tenant_id_setter ()
59
- RETURNS TRIGGER LANGUAGE plpgsql AS $$
60
- BEGIN
61
- IF NOT EXISTS (
62
- SELECT FROM #{PgRls.table_name}
63
- WHERE tenant_id = (current_setting('rls.tenant_id'))::uuid
64
- ) THEN
65
- INSERT INTO #{PgRls.table_name} (tenant_id)
66
- VALUES ((current_setting('rls.tenant_id'))::uuid);
67
- END IF;
68
-
69
- NEW.tenant_id:= (current_setting('rls.tenant_id'));
70
- RETURN NEW;
71
- END $$;
72
- SQL
73
- end
74
-
75
- def create_rls_blocking_function
76
- <<~SQL
77
- -- Create RLS Blocking Function
78
- CREATE OR REPLACE FUNCTION id_safe_guard ()
79
- RETURNS TRIGGER LANGUAGE plpgsql AS $$
80
- BEGIN
81
- RAISE EXCEPTION 'This column is guarded due to tenancy dependency';
82
- END $$;
83
- SQL
84
- end
85
-
86
- def create_rls_solo_tenant_table
87
- <<~SQL
88
- -- Create Tenant Table
89
- CREATE TABLE #{PgRls.table_name} (
90
- tenant_id uuid PRIMARY KEY
91
- );
92
- SQL
93
- end
94
-
95
- def append_blocking_function
96
- <<~SQL
97
- -- Append Blocking Function
98
- CREATE TRIGGER id_safe_guard
99
- BEFORE UPDATE OF tenant_id ON #{PgRls.table_name}
100
- FOR EACH ROW EXECUTE PROCEDURE id_safe_guard();
101
- SQL
102
- end
103
- end
104
- end
105
- end
106
- end
@@ -1,23 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module PgRls
4
- # Ensure Connection is with App_use
5
- module SecureConnection
6
- def self.establish_secure_connection
7
- return if secure_connection_established?
8
-
9
- return if PgRls.default_connection?
10
-
11
- PgRls.establish_new_connection
12
- end
13
-
14
- def self.secure_connection_established?
15
- PgRls.current_connection_username == PgRls.username
16
- end
17
-
18
- def self.included(base)
19
- establish_secure_connection
20
- base.ignored_columns = %w[tenant_id]
21
- end
22
- end
23
- end
@@ -1,50 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module PgRls
4
- module Solo
5
- # Set and Fetch Tenant without loading a model
6
- class Tenant
7
- class << self
8
- attr_reader :tenant
9
-
10
- def switch!(resource)
11
- switch_tenant!(resource)
12
- rescue StandardError => e
13
- Rails.logger.info('connection was not made')
14
- raise e
15
- end
16
-
17
- def fetch
18
- @fetch ||= PgRls.connection_class.connection.execute(
19
- "SELECT current_setting('rls.tenant_id')"
20
- ).getvalue(0, 0)
21
- end
22
-
23
- def around(resource)
24
- switch_tenant!(resource)
25
- yield
26
- ensure
27
- reset_rls!
28
- end
29
-
30
- private
31
-
32
- def reset_rls!
33
- @fetch = nil
34
- @tenant = nil
35
- PgRls.connection_class.connection.execute('RESET rls.tenant_id')
36
- end
37
-
38
- def switch_tenant!(resource)
39
- connection_adapter = PgRls.connection_class
40
-
41
- raise PgRls::Errors::TenantNotFound if resource.blank?
42
-
43
- connection_adapter.connection.execute(format('SET rls.tenant_id = %s',
44
- connection_adapter.connection.quote(resource)))
45
- "RLS changed to '#{resource}'"
46
- end
47
- end
48
- end
49
- end
50
- end