hanami 2.1.1 → 2.2.0.beta2

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