cell 0.1.1 → 0.1.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/README.md +125 -49
- data/Rakefile +2 -1
- data/cell.gemspec +1 -1
- data/lib/cell/active_job.rb +32 -0
- data/lib/cell/clone_schema.rb +18 -16
- data/lib/cell/console.rb +9 -15
- data/lib/cell/context.rb +48 -27
- data/lib/cell/meta.rb +87 -0
- data/lib/cell/migration.rb +131 -44
- data/lib/cell/model_extensions.rb +85 -0
- data/lib/cell/railtie.rb +17 -3
- data/lib/cell/sanity_check.rb +21 -23
- data/lib/cell/schema.rb +2 -13
- data/lib/cell/tasks/cell.rake +18 -11
- data/lib/cell/tenant.rb +55 -0
- data/lib/cell/url_options.rb +26 -0
- data/lib/cell/version.rb +1 -1
- data/lib/cell.rb +1 -6
- metadata +9 -11
- data/lib/cell/configure.rb +0 -70
- data/lib/cell/data_directory.rb +0 -42
- data/lib/cell/model.rb +0 -37
- data/lib/cell/sidekiq.rb +0 -58
- data/lib/cell/with_schema.rb +0 -30
- data/lib/dump/tenant_routing_manager.rb +0 -25
- data/lib/dump/tenant_url_options.rb +0 -17
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 688c58ded30105abbc900e3b92d524549b860812
|
4
|
+
data.tar.gz: e176f9fc293ad7027e53052a09561e26e11f1091
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ec3f3baee67fa98496dabaf50554f5bcfe4b230d0ae6c5d5eba21d00635e5cfeaed3156e233cce8e31289ae73a012df07090649c93e7d7b23bbc8aa95384901d
|
7
|
+
data.tar.gz: 89ef969c710585e9afb2b43ae8ef8eecc343213586e38eaae9a6a463aef45fdf4033f044781243e949fec595821c8ac2f0466c8e00d3af30ef65c182e227ed16
|
data/.gitignore
CHANGED
data/README.md
CHANGED
@@ -1,42 +1,38 @@
|
|
1
1
|
# Cell
|
2
2
|
|
3
|
-
Cell provides tenancy and customer isolation for Rails. It tries to be a
|
4
|
-
|
3
|
+
Cell provides tenancy and customer isolation for Rails. It tries to be a simpler and more thorough
|
4
|
+
implementation of things like Apartment.
|
5
5
|
|
6
|
-
The core of Cell was written years ago for Rails 1.2, before Apartment was
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
more generalized gem. Things are a bit messy now.
|
6
|
+
The core of Cell was written years ago for Rails 1.2, before Apartment was available, and has been
|
7
|
+
developed inside an application ever since. We've aggressively moved it along with the ecosystem,
|
8
|
+
and it still feels more modern than other implementations we've used. We're in the process of
|
9
|
+
making it a more generalized gem. Things are a bit messy now.
|
11
10
|
|
12
|
-
Cell tries to hook into Rails at the highest level available (e.g., ActiveJob
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
level security.)
|
11
|
+
Cell tries to hook into Rails at the highest level available (e.g., ActiveJob instead of a
|
12
|
+
particular job adapter), except for one place: PostgreSQL. I think PostgreSQL is the only widely
|
13
|
+
deployed database with the features required to make a reasonable implementation (namely: schemas,
|
14
|
+
roles, row and column level security.)
|
17
15
|
|
18
|
-
**Cell aggressively uses Ruby 2.3, PostgreSQL 9.6 and Rails 5**. This means
|
19
|
-
|
20
|
-
existing application on an older stack.
|
16
|
+
**Cell aggressively uses Ruby 2.3, PostgreSQL 9.6 and Rails 5**. This means it's probably more
|
17
|
+
appropriate for greenfield projects than bolting on to an existing application on an older stack.
|
21
18
|
|
22
|
-
Cell moves forward quickly: Don't expect to be able to hang back on an old
|
23
|
-
|
24
|
-
|
25
|
-
Rails versions.
|
19
|
+
Cell moves forward quickly: Don't expect to be able to hang back on an old version of Rails and get
|
20
|
+
new features in Cell. We version accordingly not to break things, but we don't back-port to old
|
21
|
+
Rubies, old PostgreSQLs, or old Rails versions. We'll totally change how it works overnight.
|
26
22
|
|
27
|
-
The aforementioned Apartment gem looks like perfectly viable solution for
|
28
|
-
|
23
|
+
The aforementioned Apartment gem looks like perfectly viable solution for projects with other
|
24
|
+
constraints.
|
29
25
|
|
30
26
|
What it handles:
|
31
27
|
|
32
|
-
[x] ActiveRecord
|
33
|
-
[
|
34
|
-
[ ]
|
35
|
-
[
|
36
|
-
[ ]
|
37
|
-
[ ]
|
38
|
-
[
|
39
|
-
[x] Rake
|
28
|
+
- [x] ActiveRecord models
|
29
|
+
- [x] Migrations: including shared and global tables
|
30
|
+
- [ ] ActionController: An integration point for you
|
31
|
+
- [x] ActiveJob: Jobs are executed in the proper context
|
32
|
+
- [ ] ActionMailer: Your URIs will work
|
33
|
+
- [ ] ActionView: Caching is automatically keyed on the Cell
|
34
|
+
- [ ] Redis: optional, via redis-namespace
|
35
|
+
- [x] Rake: tasks which run over each Cell
|
40
36
|
|
41
37
|
The rest of this README will be updated as the code is extracted.
|
42
38
|
|
@@ -48,38 +44,118 @@ Add this line to your application's Gemfile:
|
|
48
44
|
gem 'cell'
|
49
45
|
```
|
50
46
|
|
51
|
-
|
47
|
+
## Tests
|
52
48
|
|
53
|
-
|
49
|
+
The application Cell was ripped out of had heavy test-coverage of the functionality Cell provides,
|
50
|
+
but at the wrong level (e.g., our model, not the Cell behavior which this became). It's proven
|
51
|
+
impossible to extract anything workable out of these into this gem, but we're working on starting
|
52
|
+
from scratch.
|
54
53
|
|
55
|
-
|
54
|
+
# How it Works
|
56
55
|
|
57
|
-
|
56
|
+
The most complicated interaction Cell has with Rails exist with ActiveRecord and its migrations.
|
57
|
+
First, what happens in your PostgreSQL database:
|
58
58
|
|
59
|
-
|
59
|
+
Global tables are generated in the default schema, which we'll just call "public", and they stay
|
60
|
+
there. This is what you're used to.
|
61
|
+
|
62
|
+
Per-tenant tables are redirected to a schema called "cell_prototype".
|
63
|
+
|
64
|
+
Both of these are versioned by 'public.schema_migrations'. e.g., the prototype and public schemas
|
65
|
+
are always considered to be the same version.
|
66
|
+
|
67
|
+
When you create a new tenant, the `cell_prototype` schema is copied to a new schema for the tenant,
|
68
|
+
and the tenant gets its own 'schema_migrations' table, copied from global.
|
69
|
+
|
70
|
+
When you 'db:migrate', un-ran migrations start populating public and cell_prototype, but not
|
71
|
+
tenants.
|
72
|
+
|
73
|
+
To update existing tenants after a 'db:migrate', you need to run 'cell:db:migrate', which will run
|
74
|
+
the migrations needed to bring each tenant up to date. There are an entire suite of `cell:` rake
|
75
|
+
tasks that just run the normal task over each tenant. Check out `rake -T | grep cell:`
|
60
76
|
|
61
|
-
|
62
|
-
|
63
|
-
Cell
|
64
|
-
workable out of these into this gem, but we're working on starting from
|
65
|
-
scratch.
|
77
|
+
Cell activates a tenant by adjusting 'schema_search_path'. The way to use this is
|
78
|
+
`YourModel.use {|block }`. Note that `Cell::Model` ends up aliased to `YourModel`, so
|
79
|
+
`Cell::Model.find(...).use` works as well.
|
66
80
|
|
81
|
+
There are a lot of tricks Cell has to use re: thread safety and the query cache to make this act
|
82
|
+
normally.
|
83
|
+
|
84
|
+
In the development mode console, Cell will activate the first tenant for you before you get a REPL.
|
67
85
|
|
68
86
|
## Usage
|
69
87
|
|
70
|
-
|
88
|
+
First, read the [How it Works](#how-it-works) section. You need to know what's going on in case
|
89
|
+
something breaks.
|
90
|
+
|
91
|
+
Please don't use cell unless you're OK with the idea of digging into the code when things go wrong.
|
92
|
+
It's not a drop-in thing yet.
|
93
|
+
|
94
|
+
Feel free to open PRs or tickets, though, but I'd rather see "We ended up having to use something
|
95
|
+
else because Cell doesn't handle this" than "Cell must handle this".
|
96
|
+
|
97
|
+
I'm now committed to keeping the following section up-to-date, as the only source of piss-poor
|
98
|
+
documentation.
|
99
|
+
|
100
|
+
First, use `cell` in your `Gemfile` as stated earlier.
|
101
|
+
|
102
|
+
First, you need a migration that does the following, and only the following:
|
71
103
|
|
72
|
-
|
104
|
+
```ruby
|
105
|
+
class InitializeCell < ActiveRecord::Migration[5.0]
|
106
|
+
def change
|
107
|
+
initialize_cell!
|
108
|
+
end
|
109
|
+
end
|
110
|
+
```
|
111
|
+
|
112
|
+
Then you need a migration that creates your tenanted model:
|
113
|
+
```ruby
|
114
|
+
class CreateAccounts < ActiveRecord::Migration[5.0]
|
115
|
+
def change
|
116
|
+
global do
|
117
|
+
create_table :accounts do |t|
|
118
|
+
# ...
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
```
|
124
|
+
|
125
|
+
We do the configuration in the model so it can be updated if you're running the (very useful)
|
126
|
+
clusterfuck that is Spring: your settings will update in development.
|
127
|
+
|
128
|
+
```ruby
|
129
|
+
class Account < ApplicationRecord
|
130
|
+
extend Cell::Tenant
|
131
|
+
end
|
132
|
+
```
|
133
|
+
|
134
|
+
From there on out, most of the weird shit dealing with Cell will be in migrations. By default, it
|
135
|
+
assumes everything you do in a migration is per-tenant. If you want to escape this, or create
|
136
|
+
shared/global tables, you need to wrap it in a global block.
|
137
|
+
|
138
|
+
```ruby
|
139
|
+
class TestMigration < ActiveRecord::Migration[5.0]
|
140
|
+
def change
|
141
|
+
# This will exist in each tenant
|
142
|
+
create_table :users do |t|
|
143
|
+
t.string :name, null: false
|
144
|
+
end
|
145
|
+
|
146
|
+
# This is global, and will be shared across all tenants
|
147
|
+
global do
|
148
|
+
create_table :capabilities do |t|
|
149
|
+
t.string :name, null: false
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
154
|
+
```
|
73
155
|
|
74
|
-
|
75
|
-
|
76
|
-
to experiment.
|
156
|
+
`rake db:migrate` updates the global and prototype schema, `rake cell:db:migrate` will run the
|
157
|
+
non-global blocks across all existing tenants.
|
77
158
|
|
78
|
-
To install this gem onto your local machine, run `bundle exec rake install`. To
|
79
|
-
release a new version, update the version number in `version.rb`, and then
|
80
|
-
run `bundle exec rake release`, which will create a git tag for the version,
|
81
|
-
push git commits and tags, and push the `.gem` file to
|
82
|
-
[rubygems.org](https://rubygems.org).
|
83
159
|
|
84
160
|
## Contributing
|
85
161
|
|
data/Rakefile
CHANGED
data/cell.gemspec
CHANGED
@@ -0,0 +1,32 @@
|
|
1
|
+
module Cell
|
2
|
+
module ActiveJob
|
3
|
+
KEY = :'Cell.tenant'
|
4
|
+
|
5
|
+
def serialize
|
6
|
+
if (current_id = ::Cell::Model.current&.cell_id)
|
7
|
+
super.merge(KEY => current_id)
|
8
|
+
else
|
9
|
+
super
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def deserialize(job_data)
|
14
|
+
if job_data.key?(KEY)
|
15
|
+
self.cell_tenant = ::Cell::Model.cell_find(job_data[KEY])
|
16
|
+
end
|
17
|
+
super
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.prepended(cls)
|
21
|
+
cls.send(:attr_accessor, :cell_tenant)
|
22
|
+
cls.around_perform do |job, block|
|
23
|
+
::Cell::Model.use(job.cell_tenant, &block)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
|
30
|
+
if defined?(::ActiveJob)
|
31
|
+
::ActiveJob::Base.prepend(::Cell::ActiveJob)
|
32
|
+
end
|
data/lib/cell/clone_schema.rb
CHANGED
@@ -1,39 +1,41 @@
|
|
1
1
|
require 'active_record/schema_migration'
|
2
|
-
require 'cell/
|
2
|
+
require 'cell/meta'
|
3
3
|
|
4
4
|
module Cell
|
5
5
|
module CloneSchema
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
6
|
+
def self.connection
|
7
|
+
::ActiveRecord::Base.connection
|
8
|
+
end
|
9
|
+
|
10
|
+
|
11
|
+
def self.clone_schema(src, dst)
|
12
|
+
connection.execute <<~SQL
|
13
|
+
SELECT public.clone_schema(#{connection.quote(src)}, #{connection.quote(dst)}, TRUE)
|
10
14
|
SQL
|
11
|
-
con.execute(sql)
|
12
15
|
end
|
13
16
|
|
14
17
|
|
15
|
-
def copy_schema_migrations_to(dst
|
16
|
-
Cell::
|
17
|
-
connection: connection) do
|
18
|
+
def self.copy_schema_migrations_to(dst)
|
19
|
+
Cell::Meta.with_schema(dst, exclusive: true) do
|
18
20
|
::ActiveRecord::SchemaMigration.create_table
|
19
21
|
end
|
20
22
|
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
23
|
+
connection.execute <<~SQL
|
24
|
+
INSERT INTO #{connection.quote_schema_name(dst)}.schema_migrations
|
25
|
+
SELECT * from schema_migrations
|
26
|
+
SQL
|
25
27
|
end
|
26
28
|
|
27
29
|
|
28
|
-
def install_function!
|
30
|
+
def self.install_function!
|
29
31
|
@created ||= begin
|
30
|
-
|
32
|
+
connection.execute(function_definition)
|
31
33
|
true
|
32
34
|
end
|
33
35
|
end
|
34
36
|
|
35
37
|
|
36
|
-
def function_definition
|
38
|
+
def self.function_definition
|
37
39
|
File.read(__FILE__).split(/^__END__$/, 2)[-1]
|
38
40
|
end
|
39
41
|
end
|
data/lib/cell/console.rb
CHANGED
@@ -1,25 +1,19 @@
|
|
1
1
|
module Cell
|
2
2
|
module Console
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
if ENV['T']
|
10
|
-
Cell::Tenant.cell_find(ENV['T']).tap do |r|
|
11
|
-
fail "Couldn't locate tenant: #{ENV['T']}" if r.nil?
|
12
|
-
end
|
13
|
-
elsif Rails.env.development?
|
14
|
-
Cell::Tenant.first
|
3
|
+
def self.default_console_tenant
|
4
|
+
if ENV['T'].present?
|
5
|
+
Cell::Model.cell_find!(ENV['T'])
|
6
|
+
elsif Rails.env.development? && ENV['T'] != ''
|
7
|
+
Cell::Model.first
|
15
8
|
end
|
16
9
|
end
|
17
10
|
|
18
|
-
def configure!
|
11
|
+
def self.configure!
|
19
12
|
if (t = default_console_tenant)
|
20
|
-
::Cell::
|
13
|
+
::Cell::Model.set!(t)
|
21
14
|
end
|
22
15
|
end
|
23
|
-
|
24
16
|
end
|
25
17
|
end
|
18
|
+
|
19
|
+
::Cell::Console.configure!
|
data/lib/cell/context.rb
CHANGED
@@ -1,46 +1,65 @@
|
|
1
|
-
|
2
|
-
require '
|
1
|
+
require 'cell/meta'
|
2
|
+
require 'active_support/callbacks'
|
3
3
|
|
4
4
|
module Cell
|
5
5
|
module Context
|
6
6
|
THREAD_KEY = :'Cell.cell_id'
|
7
7
|
|
8
8
|
module ClassMethods
|
9
|
+
def self.extended(cls)
|
10
|
+
cls.include ActiveSupport::Callbacks
|
11
|
+
cls.define_callbacks :set_tenant, :use_tenant
|
12
|
+
end
|
13
|
+
|
14
|
+
def after_set(*args, &block)
|
15
|
+
set_callback(:set_tenant, :after, *args, &block)
|
16
|
+
end
|
17
|
+
|
18
|
+
def around_use(*args, &block)
|
19
|
+
set_callback(:use_tenant, :around, *args, &block)
|
20
|
+
end
|
21
|
+
|
9
22
|
def current
|
10
23
|
Thread.current[THREAD_KEY]
|
11
24
|
end
|
12
25
|
|
13
|
-
def
|
14
|
-
|
15
|
-
|
16
|
-
|
26
|
+
def set!(tenant, exclusive: false)
|
27
|
+
::Cell::Meta.with_schema(tenant.schema_name, exclusive: exclusive)
|
28
|
+
set_current(tenant)
|
29
|
+
tenant.run_callbacks :set_tenant
|
30
|
+
tenant
|
31
|
+
end
|
17
32
|
|
18
|
-
|
33
|
+
def use(tenant, exclusive: false)
|
34
|
+
fail ArgumentError, "block required" unless block_given?
|
35
|
+
saved_tenant = current
|
19
36
|
|
20
|
-
if
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
connection: connection) do
|
26
|
-
set_current(tenant)
|
27
|
-
yield Cell::Tenant.current
|
37
|
+
if tenant
|
38
|
+
::Cell::Meta.with_schema(tenant.schema_name, exclusive: exclusive) do
|
39
|
+
set_current(tenant)
|
40
|
+
tenant.run_callbacks :use_tenant do
|
41
|
+
yield Cell::Model.current
|
28
42
|
end
|
29
|
-
ensure
|
30
|
-
set_current(saved_tenant)
|
31
43
|
end
|
32
44
|
else
|
33
|
-
::Cell::
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
tenant
|
45
|
+
::Cell::Meta.with_global_schema do
|
46
|
+
set_current(nil)
|
47
|
+
yield nil
|
48
|
+
end
|
38
49
|
end
|
50
|
+
|
51
|
+
tenant
|
52
|
+
ensure
|
53
|
+
set_current(saved_tenant)
|
39
54
|
end
|
40
55
|
|
41
56
|
private
|
42
|
-
def set_current(
|
43
|
-
|
57
|
+
def set_current(tenant)
|
58
|
+
unless tenant.nil? || tenant.is_a?(Cell::Model)
|
59
|
+
fail ArgumentError, "Invalid tenant: #{tenant.inspect}"
|
60
|
+
end
|
61
|
+
|
62
|
+
Thread.current[THREAD_KEY] = tenant
|
44
63
|
end
|
45
64
|
end
|
46
65
|
|
@@ -48,14 +67,16 @@ module Cell
|
|
48
67
|
self.class.use(self, *args, &block)
|
49
68
|
end
|
50
69
|
|
70
|
+
def set!(tenant)
|
71
|
+
Cell::Model.set!(tenant)
|
72
|
+
end
|
73
|
+
|
51
74
|
def current?
|
52
75
|
self.class.current == self
|
53
76
|
end
|
54
77
|
|
55
78
|
def self.prepended(cls)
|
56
|
-
|
57
|
-
prepend ClassMethods
|
58
|
-
end
|
79
|
+
cls.extend(ClassMethods)
|
59
80
|
end
|
60
81
|
end
|
61
82
|
end
|
data/lib/cell/meta.rb
ADDED
@@ -0,0 +1,87 @@
|
|
1
|
+
require 'cell/schema'
|
2
|
+
require 'active_record/internal_metadata'
|
3
|
+
|
4
|
+
module Cell
|
5
|
+
module Meta
|
6
|
+
GLOBAL_KEY = 'Cell.global'
|
7
|
+
|
8
|
+
def self.connection
|
9
|
+
::ActiveRecord::Base.connection
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.with_schema(new_path, exclusive: false)
|
13
|
+
saved_path = connection.schema_search_path
|
14
|
+
active_path = exclusive ? new_path : "#{new_path},#{saved_path}"
|
15
|
+
|
16
|
+
set_schema_search_path(active_path)
|
17
|
+
|
18
|
+
if block_given?
|
19
|
+
begin
|
20
|
+
yield
|
21
|
+
ensure
|
22
|
+
set_schema_search_path(saved_path)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.set_schema_search_path(path)
|
28
|
+
if path != connection.schema_search_path
|
29
|
+
connection.schema_search_path = path
|
30
|
+
connection.clear_query_cache
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.global_schema
|
35
|
+
@global_schema ||= ActiveRecord::Base.connection.schema_search_path
|
36
|
+
end
|
37
|
+
|
38
|
+
def self.prototype_schema
|
39
|
+
'cell_prototype'
|
40
|
+
end
|
41
|
+
|
42
|
+
def self.structural_schema
|
43
|
+
"#{prototype_schema}, #{global_schema}"
|
44
|
+
end
|
45
|
+
|
46
|
+
def self.with_global_schema(&block)
|
47
|
+
with_schema(global_schema, exclusive: true, &block)
|
48
|
+
end
|
49
|
+
|
50
|
+
def self.with_prototype_schema(&block)
|
51
|
+
with_schema(prototype_schema, exclusive: true, &block)
|
52
|
+
end
|
53
|
+
|
54
|
+
def self.with_structural_schema(&block)
|
55
|
+
with_schema(structural_schema, exclusive: true, &block)
|
56
|
+
end
|
57
|
+
|
58
|
+
def self.global_tables
|
59
|
+
with_global_schema do
|
60
|
+
(::ActiveRecord::InternalMetadata[GLOBAL_KEY] || '').split(',')
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def self.set_global_tables(tables)
|
65
|
+
with_global_schema do
|
66
|
+
table_string = tables.map(&:to_s).sort.uniq.join(',')
|
67
|
+
::ActiveRecord::InternalMetadata[GLOBAL_KEY] = table_string
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def self.add_global_table(name)
|
72
|
+
set_global_tables(global_tables + [name.to_s])
|
73
|
+
end
|
74
|
+
|
75
|
+
def self.remove_global_table(name)
|
76
|
+
set_global_tables(global_tables - [name.to_s])
|
77
|
+
end
|
78
|
+
|
79
|
+
def self.global_model?(m)
|
80
|
+
@model_map ||= global_tables.map do |k|
|
81
|
+
[k, true]
|
82
|
+
end.to_h
|
83
|
+
|
84
|
+
@model_map[m.table_name] || false
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|