hanami 2.1.1 → 2.2.0.beta1

Sign up to get free protection for your applications and to get access to all the features.
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