hanami 2.1.1 → 2.2.0.beta2

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