hanami 2.1.1 → 2.2.0.beta1

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.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +19 -0
  3. data/README.md +7 -7
  4. data/hanami.gemspec +6 -6
  5. data/lib/hanami/app.rb +5 -1
  6. data/lib/hanami/config/db.rb +33 -0
  7. data/lib/hanami/config.rb +36 -9
  8. data/lib/hanami/extensions/db/repo.rb +103 -0
  9. data/lib/hanami/extensions.rb +4 -0
  10. data/lib/hanami/helpers/form_helper/form_builder.rb +2 -4
  11. data/lib/hanami/provider_registrar.rb +26 -0
  12. data/lib/hanami/providers/assets.rb +2 -20
  13. data/lib/hanami/providers/db/adapter.rb +68 -0
  14. data/lib/hanami/providers/db/adapters.rb +44 -0
  15. data/lib/hanami/providers/db/config.rb +66 -0
  16. data/lib/hanami/providers/db/sql_adapter.rb +80 -0
  17. data/lib/hanami/providers/db.rb +203 -0
  18. data/lib/hanami/providers/db_logging.rb +22 -0
  19. data/lib/hanami/providers/rack.rb +1 -1
  20. data/lib/hanami/providers/relations.rb +31 -0
  21. data/lib/hanami/providers/routes.rb +1 -13
  22. data/lib/hanami/rake_tasks.rb +8 -7
  23. data/lib/hanami/slice.rb +84 -4
  24. data/lib/hanami/version.rb +1 -1
  25. data/lib/hanami.rb +3 -0
  26. data/spec/integration/container/provider_environment_spec.rb +52 -0
  27. data/spec/integration/db/auto_registration_spec.rb +39 -0
  28. data/spec/integration/db/db_inflector_spec.rb +57 -0
  29. data/spec/integration/db/db_slices_spec.rb +327 -0
  30. data/spec/integration/db/db_spec.rb +220 -0
  31. data/spec/integration/db/logging_spec.rb +238 -0
  32. data/spec/integration/db/provider_config_spec.rb +88 -0
  33. data/spec/integration/db/provider_spec.rb +35 -0
  34. data/spec/integration/db/repo_spec.rb +215 -0
  35. data/spec/integration/db/slices_importing_from_parent.rb +130 -0
  36. data/spec/integration/slices/slice_configuration_spec.rb +4 -4
  37. data/spec/support/app_integration.rb +3 -0
  38. data/spec/unit/hanami/config/db_spec.rb +38 -0
  39. data/spec/unit/hanami/config/router_spec.rb +1 -1
  40. data/spec/unit/hanami/helpers/form_helper_spec.rb +31 -0
  41. data/spec/unit/hanami/providers/db/config/default_config_spec.rb +107 -0
  42. data/spec/unit/hanami/providers/db/config_spec.rb +206 -0
  43. data/spec/unit/hanami/slice_spec.rb +32 -0
  44. data/spec/unit/hanami/version_spec.rb +1 -1
  45. metadata +61 -19
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/system"
4
+
5
+ RSpec.describe "ROM::Inflector", :app_integration do
6
+ before do
7
+ @env = ENV.to_h
8
+ allow(Hanami::Env).to receive(:loaded?).and_return(false)
9
+ end
10
+
11
+ after do
12
+ ENV.replace(@env)
13
+ end
14
+
15
+ around :each do |example|
16
+ inflector = ROM::Inflector
17
+ ROM.instance_eval {
18
+ remove_const :Inflector
19
+ const_set :Inflector, Dry::Inflector.new
20
+ }
21
+ example.run
22
+ ensure
23
+ ROM.instance_eval {
24
+ remove_const :Inflector
25
+ const_set :Inflector, inflector
26
+ }
27
+ end
28
+
29
+ it "replaces ROM::Inflector with the Hanami inflector" do
30
+ with_tmp_directory(Dir.mktmpdir) do
31
+ write "config/app.rb", <<~RUBY
32
+ require "hanami"
33
+
34
+ module TestApp
35
+ class App < Hanami::App
36
+ end
37
+ end
38
+ RUBY
39
+
40
+ write "app/relations/posts.rb", <<~RUBY
41
+ module TestApp
42
+ module Relations
43
+ class Posts < Hanami::DB::Relation
44
+ schema :posts, infer: true
45
+ end
46
+ end
47
+ end
48
+ RUBY
49
+
50
+ ENV["DATABASE_URL"] = "sqlite::memory"
51
+
52
+ require "hanami/prepare"
53
+
54
+ expect { Hanami.app.prepare :db }.to change { ROM::Inflector == Hanami.app["inflector"] }.from(false).to(true)
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,327 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe "DB / Slices", :app_integration do
4
+ before do
5
+ @env = ENV.to_h
6
+ allow(Hanami::Env).to receive(:loaded?).and_return(false)
7
+ end
8
+
9
+ after do
10
+ ENV.replace(@env)
11
+ end
12
+
13
+ specify "slices using the same database_url and extensions share a gateway/connection" do
14
+ with_tmp_directory(@dir = Dir.mktmpdir) do
15
+ write "config/app.rb", <<~RUBY
16
+ require "hanami"
17
+
18
+ module TestApp
19
+ class App < Hanami::App
20
+ end
21
+ end
22
+ RUBY
23
+
24
+ write "slices/admin/relations/.keep", ""
25
+ write "slices/main/relations/.keep", ""
26
+
27
+ ENV["DATABASE_URL"] = "sqlite::memory"
28
+
29
+ require "hanami/prepare"
30
+
31
+ expect(Admin::Slice["db.rom"].gateways[:default]).to be Main::Slice["db.rom"].gateways[:default]
32
+ end
33
+ end
34
+
35
+ specify "slices using the same database_url but different extensions have distinct gateways/connections" do
36
+ with_tmp_directory(@dir = Dir.mktmpdir) do
37
+ write "config/app.rb", <<~RUBY
38
+ require "hanami"
39
+
40
+ module TestApp
41
+ class App < Hanami::App
42
+ end
43
+ end
44
+ RUBY
45
+
46
+ write "slices/admin/config/providers/db.rb", <<~RUBY
47
+ Admin::Slice.configure_provider :db do
48
+ config.adapter :sql do |a|
49
+ a.extensions.clear
50
+ end
51
+ end
52
+ RUBY
53
+
54
+ write "slices/main/config/providers/db.rb", <<~RUBY
55
+ Main::Slice.configure_provider :db do
56
+ config.adapter :sql do |a|
57
+ a.extensions.clear
58
+ a.extension :error_sql
59
+ end
60
+ end
61
+ RUBY
62
+
63
+ ENV["DATABASE_URL"] = "sqlite::memory"
64
+
65
+ require "hanami/prepare"
66
+
67
+ # Different gateways, due to the distinct extensions
68
+ expect(Admin::Slice["db.rom"].gateways[:default]).not_to be Main::Slice["db.rom"].gateways[:default]
69
+
70
+ # Even though their URIs are the same
71
+ expect(Admin::Slice["db.rom"].gateways[:default].connection.opts[:uri])
72
+ .to eq Main::Slice["db.rom"].gateways[:default].connection.opts[:uri]
73
+ end
74
+ end
75
+
76
+ specify "using separate relations per slice, while sharing config from the app" do
77
+ with_tmp_directory(@dir = Dir.mktmpdir) do
78
+ write "config/app.rb", <<~RUBY
79
+ require "hanami"
80
+
81
+ module TestApp
82
+ class App < Hanami::App
83
+ end
84
+ end
85
+ RUBY
86
+
87
+ write "config/db/.keep", ""
88
+
89
+ write "config/providers/db.rb", <<~RUBY
90
+ Hanami.app.configure_provider :db do
91
+ config.adapter :sql do |a|
92
+ # a.skip_defaults
93
+ # a.plugin relation: :auto_restrictions
94
+ a.extension :exclude_or_null
95
+ end
96
+ end
97
+ RUBY
98
+
99
+ write "slices/admin/relations/posts.rb", <<~RUBY
100
+ module Admin
101
+ module Relations
102
+ class Posts < Hanami::DB::Relation
103
+ schema :posts, infer: true
104
+ end
105
+ end
106
+ end
107
+ RUBY
108
+
109
+ write "slices/admin/relations/authors.rb", <<~RUBY
110
+ module Admin
111
+ module Relations
112
+ class Authors < Hanami::DB::Relation
113
+ schema :authors, infer: true
114
+ end
115
+ end
116
+ end
117
+ RUBY
118
+
119
+ write "slices/main/config/providers/db.rb", <<~RUBY
120
+ Main::Slice.configure_provider :db do
121
+ config.adapter :sql do |a|
122
+ a.extensions.clear
123
+ end
124
+ end
125
+ RUBY
126
+
127
+ write "slices/main/relations/posts.rb", <<~RUBY
128
+ module Main
129
+ module Relations
130
+ class Posts < Hanami::DB::Relation
131
+ schema :posts, infer: true
132
+ end
133
+ end
134
+ end
135
+ RUBY
136
+
137
+ ENV["DATABASE_URL"] = "sqlite://" + Pathname(@dir).realpath.join("database.db").to_s
138
+
139
+ require "hanami/prepare"
140
+
141
+ Main::Slice.prepare :db
142
+
143
+ expect(Main::Slice["db.config"]).to be_an_instance_of ROM::Configuration
144
+ expect(Main::Slice["db.gateway"]).to be_an_instance_of ROM::SQL::Gateway
145
+
146
+ expect(Admin::Slice.registered?("db.config")).to be false
147
+
148
+ Admin::Slice.prepare :db
149
+
150
+ expect(Admin::Slice["db.config"]).to be_an_instance_of ROM::Configuration
151
+ expect(Admin::Slice["db.gateway"]).to be_an_instance_of ROM::SQL::Gateway
152
+
153
+ # Manually run a migration and add a test record
154
+ gateway = Admin::Slice["db.gateway"]
155
+ migration = gateway.migration do
156
+ change do
157
+ create_table :posts do
158
+ primary_key :id
159
+ column :title, :text, null: false
160
+ end
161
+
162
+ create_table :authors do
163
+ primary_key :id
164
+ end
165
+ end
166
+ end
167
+ migration.apply(gateway, :up)
168
+ gateway.connection.execute("INSERT INTO posts (title) VALUES ('Together breakfast')")
169
+
170
+ # Admin slice has appropriate relations registered, and can access data
171
+ expect(Admin::Slice["db.rom"].relations[:posts].to_a).to eq [{id: 1, title: "Together breakfast"}]
172
+ expect(Admin::Slice["relations.posts"]).to be Admin::Slice["db.rom"].relations[:posts]
173
+ expect(Admin::Slice["relations.authors"]).to be Admin::Slice["db.rom"].relations[:authors]
174
+
175
+ # Main slice can access data, and only has its own relations (no crossover from admin slice)
176
+ expect(Main::Slice["db.rom"].relations[:posts].to_a).to eq [{id: 1, title: "Together breakfast"}]
177
+ expect(Main::Slice["relations.posts"]).to be Main::Slice["db.rom"].relations[:posts]
178
+ expect(Main::Slice["db.rom"].relations.elements.keys).not_to include :authors
179
+ expect(Main::Slice["relations.posts"]).not_to be Admin::Slice["relations.posts"]
180
+
181
+ # Config in the app's db provider is copied to child slice providers
182
+ expect(Admin::Slice["db.gateway"].options[:extensions]).to eq [
183
+ :exclude_or_null,
184
+ :caller_logging,
185
+ :error_sql,
186
+ :sql_comments,
187
+ ]
188
+ # Except when it has been explicitly configured in a child slice provider
189
+ expect(Main::Slice["db.gateway"].options[:extensions]).to eq []
190
+
191
+ # Plugins configured in the app's db provider are copied to child slice providers
192
+ expect(Admin::Slice["db.config"].setup.plugins.length).to eq 2
193
+ expect(Admin::Slice["db.config"].setup.plugins).to include an_object_satisfying { |plugin|
194
+ plugin.type == :relation && plugin.name == :auto_restrictions
195
+ }
196
+ expect(Admin::Slice["db.config"].setup.plugins).to include an_object_satisfying { |plugin|
197
+ plugin.type == :relation && plugin.name == :instrumentation
198
+ }
199
+
200
+ expect(Main::Slice["db.config"].setup.plugins).to eq Admin::Slice["db.config"].setup.plugins
201
+ end
202
+ end
203
+
204
+ specify "disabling sharing of config from the app" do
205
+ with_tmp_directory(@dir = Dir.mktmpdir) do
206
+ write "config/app.rb", <<~RUBY
207
+ require "hanami"
208
+
209
+ module TestApp
210
+ class App < Hanami::App
211
+ end
212
+ end
213
+ RUBY
214
+
215
+ write "config/providers/db.rb", <<~RUBY
216
+ Hanami.app.configure_provider :db do
217
+ config.adapter :sql do |a|
218
+ a.extension :exclude_or_null
219
+ end
220
+ end
221
+ RUBY
222
+
223
+ write "config/slices/admin.rb", <<~RUBY
224
+ module Admin
225
+ class Slice < Hanami::Slice
226
+ config.db.configure_from_parent = false
227
+ end
228
+ end
229
+ RUBY
230
+
231
+ write "slices/admin/config/providers/db.rb", <<~RUBY
232
+ Admin::Slice.configure_provider :db do
233
+ end
234
+ RUBY
235
+
236
+ ENV["DATABASE_URL"] = "sqlite://" + Pathname(@dir).realpath.join("database.db").to_s
237
+
238
+ require "hanami/prepare"
239
+
240
+ expect(Admin::Slice["db.gateway"].options[:extensions]).not_to include :exclude_or_null
241
+ end
242
+ end
243
+
244
+ specify "slices using separate databases" do
245
+ with_tmp_directory(@dir = Dir.mktmpdir) do
246
+ write "config/app.rb", <<~RUBY
247
+ require "hanami"
248
+
249
+ module TestApp
250
+ class App < Hanami::App
251
+ end
252
+ end
253
+ RUBY
254
+
255
+ write "slices/admin/relations/posts.rb", <<~RUBY
256
+ module Admin
257
+ module Relations
258
+ class Posts < Hanami::DB::Relation
259
+ schema :posts, infer: true
260
+ end
261
+ end
262
+ end
263
+ RUBY
264
+
265
+ write "slices/admin/slices/super/relations/posts.rb", <<~RUBY
266
+ module Admin
267
+ module Super
268
+ module Relations
269
+ class Posts < Hanami::DB::Relation
270
+ schema :posts, infer: true
271
+ end
272
+ end
273
+ end
274
+ end
275
+ RUBY
276
+
277
+ write "slices/main/relations/posts.rb", <<~RUBY
278
+ module Main
279
+ module Relations
280
+ class Posts < Hanami::DB::Relation
281
+ schema :posts, infer: true
282
+ end
283
+ end
284
+ end
285
+ RUBY
286
+
287
+ ENV["ADMIN__DATABASE_URL"] = "sqlite://" + Pathname(@dir).realpath.join("admin.db").to_s
288
+ ENV["ADMIN__SUPER__DATABASE_URL"] = "sqlite://" + Pathname(@dir).realpath.join("admin_super.db").to_s
289
+ ENV["MAIN__DATABASE_URL"] = "sqlite://" + Pathname(@dir).realpath.join("main.db").to_s
290
+
291
+ require "hanami/prepare"
292
+
293
+ Admin::Slice.prepare :db
294
+ Admin::Super::Slice.prepare :db
295
+ Main::Slice.prepare :db
296
+
297
+ # Manually run a migration and add a test record in each slice's database
298
+ gateways = [
299
+ Admin::Slice["db.gateway"],
300
+ Admin::Super::Slice["db.gateway"],
301
+ Main::Slice["db.gateway"]
302
+ ]
303
+ gateways.each do |gateway|
304
+ migration = gateway.migration do
305
+ change do
306
+ create_table :posts do
307
+ primary_key :id
308
+ column :title, :text, null: false
309
+ end
310
+
311
+ create_table :authors do
312
+ primary_key :id
313
+ end
314
+ end
315
+ end
316
+ migration.apply(gateway, :up)
317
+ end
318
+ gateways[0].connection.execute("INSERT INTO posts (title) VALUES ('Gem glow')")
319
+ gateways[1].connection.execute("INSERT INTO posts (title) VALUES ('Cheeseburger backpack')")
320
+ gateways[2].connection.execute("INSERT INTO posts (title) VALUES ('Together breakfast')")
321
+
322
+ expect(Admin::Slice["relations.posts"].to_a).to eq [{id: 1, title: "Gem glow"}]
323
+ expect(Admin::Super::Slice["relations.posts"].to_a).to eq [{id: 1, title: "Cheeseburger backpack"}]
324
+ expect(Main::Slice["relations.posts"].to_a).to eq [{id: 1, title: "Together breakfast"}]
325
+ end
326
+ end
327
+ end
@@ -0,0 +1,220 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe "DB", :app_integration do
4
+ before do
5
+ @env = ENV.to_h
6
+ allow(Hanami::Env).to receive(:loaded?).and_return(false)
7
+ end
8
+
9
+ after do
10
+ ENV.replace(@env)
11
+ end
12
+
13
+ it "sets up ROM and reigsters relations" do
14
+ with_tmp_directory(Dir.mktmpdir) do
15
+ write "config/app.rb", <<~RUBY
16
+ require "hanami"
17
+
18
+ module TestApp
19
+ class App < Hanami::App
20
+ end
21
+ end
22
+ RUBY
23
+
24
+ write "app/relations/posts.rb", <<~RUBY
25
+ module TestApp
26
+ module Relations
27
+ class Posts < Hanami::DB::Relation
28
+ schema :posts, infer: true
29
+ end
30
+ end
31
+ end
32
+ RUBY
33
+
34
+ ENV["DATABASE_URL"] = "sqlite::memory"
35
+
36
+ require "hanami/prepare"
37
+
38
+ Hanami.app.prepare :db
39
+
40
+ expect(Hanami.app["db.config"]).to be_an_instance_of ROM::Configuration
41
+ expect(Hanami.app["db.gateway"]).to be_an_instance_of ROM::SQL::Gateway
42
+
43
+ # Manually run a migration and add a test record
44
+ gateway = Hanami.app["db.gateway"]
45
+ migration = gateway.migration do
46
+ change do
47
+ # drop_table? :posts
48
+ create_table :posts do
49
+ primary_key :id
50
+ column :title, :text, null: false
51
+ end
52
+ end
53
+ end
54
+ migration.apply(gateway, :up)
55
+ gateway.connection.execute("INSERT INTO posts (title) VALUES ('Together breakfast')")
56
+
57
+ Hanami.app.boot
58
+
59
+ expect(Hanami.app["db.rom"].relations[:posts].to_a).to eq [{id: 1, title: "Together breakfast"}]
60
+ expect(Hanami.app["relations.posts"]).to be Hanami.app["db.rom"].relations[:posts]
61
+ end
62
+ end
63
+
64
+ it "provides access in a non-booted app" do
65
+ with_tmp_directory(Dir.mktmpdir) do
66
+ write "config/app.rb", <<~RUBY
67
+ require "hanami"
68
+
69
+ module TestApp
70
+ class App < Hanami::App
71
+ end
72
+ end
73
+ RUBY
74
+
75
+ write "app/relations/posts.rb", <<~RUBY
76
+ module TestApp
77
+ module Relations
78
+ class Posts < Hanami::DB::Relation
79
+ schema :posts, infer: true
80
+ end
81
+ end
82
+ end
83
+ RUBY
84
+
85
+ ENV["DATABASE_URL"] = "sqlite::memory"
86
+
87
+ require "hanami/prepare"
88
+
89
+ Hanami.app.prepare :db
90
+
91
+ expect(Hanami.app["db.config"]).to be_an_instance_of ROM::Configuration
92
+ expect(Hanami.app["db.gateway"]).to be_an_instance_of ROM::SQL::Gateway
93
+
94
+ # Manually run a migration and add a test record
95
+ gateway = Hanami.app["db.gateway"]
96
+ migration = gateway.migration do
97
+ change do
98
+ # drop_table? :posts
99
+ create_table :posts do
100
+ primary_key :id
101
+ column :title, :text, null: false
102
+ end
103
+ end
104
+ end
105
+ migration.apply(gateway, :up)
106
+ gateway.connection.execute("INSERT INTO posts (title) VALUES ('Together breakfast')")
107
+
108
+ expect(Hanami.app["db.rom"].relations[:posts].to_a).to eq [{id: 1, title: "Together breakfast"}]
109
+ expect(Hanami.app["relations.posts"]).to be Hanami.app["db.rom"].relations[:posts]
110
+ end
111
+ end
112
+
113
+ it "raises an error when no database URL is provided" do
114
+ with_tmp_directory(Dir.mktmpdir) do
115
+ write "config/app.rb", <<~RUBY
116
+ require "hanami"
117
+
118
+ module TestApp
119
+ class App < Hanami::App
120
+ config.inflections do |inflections|
121
+ end
122
+ end
123
+ end
124
+ RUBY
125
+
126
+ write "app/relations/.keep", ""
127
+
128
+ require "hanami/prepare"
129
+
130
+ expect { Hanami.app.prepare :db }.to raise_error(Hanami::ComponentLoadError, /database_url/)
131
+ end
132
+ end
133
+
134
+ it "allows the user to configure the provider" do
135
+ with_tmp_directory(Dir.mktmpdir) do
136
+ write "config/app.rb", <<~RUBY
137
+ require "hanami"
138
+
139
+ module TestApp
140
+ class App < Hanami::App
141
+ end
142
+ end
143
+ RUBY
144
+
145
+ write "app/relations/posts.rb", <<~RUBY
146
+ module TestApp
147
+ module Relations
148
+ class Posts < Hanami::DB::Relation
149
+ schema :posts, infer: true
150
+ end
151
+ end
152
+ end
153
+ RUBY
154
+
155
+ write "config/providers/db.rb", <<~RUBY
156
+ Hanami.app.configure_provider :db do
157
+ configure do |config|
158
+ # In this test, we're not setting an ENV["DATABASE_URL"], and instead configuring
159
+ # it via the provider source config, to prove that this works
160
+
161
+ config.database_url = "sqlite::memory"
162
+ end
163
+ end
164
+ RUBY
165
+
166
+ require "hanami/prepare"
167
+
168
+ Hanami.app.prepare :db
169
+ gateway = Hanami.app["db.gateway"]
170
+ migration = gateway.migration do
171
+ change do
172
+ # drop_table? :posts
173
+ create_table :posts do
174
+ primary_key :id
175
+ column :title, :text, null: false
176
+ end
177
+ end
178
+ end
179
+ migration.apply(gateway, :up)
180
+ gateway.connection.execute("INSERT INTO posts (title) VALUES ('Together breakfast')")
181
+
182
+ Hanami.app.boot
183
+
184
+ expect(Hanami.app["db.rom"].relations[:posts].to_a).to eq [{id: 1, title: "Together breakfast"}]
185
+ expect(Hanami.app["relations.posts"]).to be Hanami.app["db.rom"].relations[:posts]
186
+ end
187
+ end
188
+
189
+ it "transforms the database URL in test mode" do
190
+ with_tmp_directory(Dir.mktmpdir) do
191
+ write "config/app.rb", <<~RUBY
192
+ require "hanami"
193
+
194
+ module TestApp
195
+ class App < Hanami::App
196
+ end
197
+ end
198
+ RUBY
199
+
200
+ write "app/relations/posts.rb", <<~RUBY
201
+ module TestApp
202
+ module Relations
203
+ class Posts < Hanami::DB::Relation
204
+ schema :posts, infer: true
205
+ end
206
+ end
207
+ end
208
+ RUBY
209
+
210
+ ENV["HANAMI_ENV"] = "test"
211
+ ENV["DATABASE_URL"] = "sqlite://./development.db"
212
+
213
+ require "hanami/prepare"
214
+
215
+ Hanami.app.prepare :db
216
+
217
+ expect(Hanami.app["db.gateway"].connection.url).to eq "sqlite://./test.db"
218
+ end
219
+ end
220
+ end